Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
1526 lines (1372 loc) · 88.5 KB
/
Copy pathMakefile
File metadata and controls
1526 lines (1372 loc) · 88.5 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
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Clerum — Root Makefile
# Orchestrates build, test, and deployment across all services.
#
# DEPLOYMENT GUIDE: docs/deploy/MINIKUBE-DEPLOY-GUIDE.md
#
# ─── JWT AUTH CHAIN (CRITICAL) ──────────────────────────────────────────────
#
# control-api → issues RPC token → Desktop App
# iss=control-api
# aud=rpc-proxy ← NOTE: audience is "rpc-proxy", not "mcp-host"
# TTL=300s
#
# Desktop App → Bearer <rpc_token> → rpc-proxy:8094
# rpc-proxy → Bearer <rpc_token> → chatllm:8080 (SAME token, passthrough)
# chatllm → validates token with iss=control-api, aud=rpc-proxy
#
# ALL THREE keys must be the same RSA-4096 pair:
# rpc-proxy-secrets.RPC_PROXY_JWT_PUBLIC_KEY
# mcp-host-config.CLERUM_AUTH_JWT_PUBLIC_KEY
# jwt-signing-keys.CONTROL_API_RPC_JWT_PRIVATE_KEY (private side)
#
# After `make minikube-gen-keys`: run `make minikube-sync-auth-key` to push
# the new public key into mcp-host-config and restart affected pods.
#
# ────────────────────────────────────────────────────────────────────────────
SHELL := /bin/bash
.DEFAULT_GOAL := help
# ── Service directories ──────────────────────────────────────────────
SERVICES := \
channel-reader \
workflow-approval-request-reader \
mcp-host \
host-context-controller \
workflow-recipes \
control-api \
external-rest-api \
rpc-proxy \
mcp-proxy \
webhook-proxy \
codex-llm-proxy \
grok-llm-proxy \
webhook-gateway \
stdio-bridge \
profile-ui \
desktop-app \
profile-ui \
mcp-servers \
packages/desktop-app-links \
packages/gfs-interaction-policy \
packages/workflow-runtime-core \
packages/workflow-sdk \
packages/llm-provider-attempt-contract \
packages/llm-providers \
packages/grok-provider-attempt-contract
# Services that have unit tests
TEST_SERVICES := \
workflow-approval-request-reader \
mcp-host \
host-context-controller \
workflow-recipes \
control-api \
external-rest-api \
rpc-proxy \
mcp-proxy \
webhook-proxy \
codex-llm-proxy \
grok-llm-proxy \
webhook-gateway \
stdio-bridge \
profile-ui \
desktop-app \
mcp-servers \
packages/desktop-app-links \
packages/gfs-interaction-policy \
packages/workflow-runtime-core \
packages/workflow-sdk \
packages/network-policy-core \
packages/llm-provider-attempt-contract \
packages/llm-providers \
packages/grok-provider-attempt-contract
# ── Optional private infra (gcp-*, promotion) ──────────────────────────────
-include Makefile.infra
# ── Optional OSS-launch tooling (public snapshot / infra carve) — monorepo-only
-include Makefile.oss
# ── Prerequisites ────────────────────────────────────────────────────
.PHONY: prereqs
prereqs: ## Check every dependency for minikube-setup up front (Docker/minikube/kubectl/node/…) with per-platform install commands
@bash scripts/check-prereqs.sh
.PHONY: doctor
doctor: prereqs ## Alias for 'prereqs'
# ── Install ──────────────────────────────────────────────────────────
.PHONY: install-git-hooks
install-git-hooks: ## Configure Git to use tracked hooks from .githooks
@git config core.hooksPath .githooks
@chmod +x .githooks/pre-commit
@chmod +x .githooks/commit-msg
@echo "Git hooks path set to .githooks"
.PHONY: install-all
install-all: ## npm install in all services (parallel)
@npm install --no-audit --no-fund
@echo "Installing dependencies across all services..."
@for svc in $(SERVICES); do \
( cd $$svc && npm install --no-audit --no-fund ) & \
done; \
wait
@cd tests/e2e && npm install --no-audit --no-fund
@echo "All installs complete."
# ── Unit Tests ───────────────────────────────────────────────────────
.PHONY: test-unit-all
test-unit-all: ## Run unit tests across all services
@echo "Running unit tests..."
@failed=""; \
for svc in $(TEST_SERVICES); do \
echo "── $$svc ──"; \
( cd $$svc && npm test ) || failed="$$failed $$svc"; \
done; \
if [ -n "$$failed" ]; then \
echo "FAILED:$$failed"; exit 1; \
fi
@echo "All unit tests passed."
.PHONY: test-codex-subscription-t0
test-codex-subscription-t0: ## Run the Codex subscription T0 aggregator (counts, no skips)
@bash scripts/tests/test-codex-subscription-t0.sh
.PHONY: test-llm-subscription-extract-t0
test-llm-subscription-extract-t0: ## Run the wave-1 oauth-broker extract T0 aggregator
@bash scripts/tests/test-llm-subscription-extract-t0.sh
.PHONY: test-grok-subscription-t0
test-grok-subscription-t0: ## Run the Grok subscription T0 aggregator (counts, no skips)
@bash scripts/tests/test-grok-subscription-t0.sh
# ── Build Preflight ──────────────────────────────────────────────────
.PHONY: build-preflight
build-preflight: ## Run local build preflight across deployable packages
@bash scripts/build-preflight.sh
# ── Minikube Cluster ─────────────────────────────────────────────────
MINIKUBE_PROFILE ?= clerum-test
# Startup supports the documented shared local profile before a branch-owned
# T2 lease exists. The mode is passed only by minikube-start; standalone auth
# sync remains lease-protected.
MINIKUBE_STARTUP_AUTH_SYNC_MODE ?= locked
MINIKUBE_MULTI_NODE ?= false
MINIKUBE_NODES ?=
MINIKUBE_MEMORY ?= 10240
MINIKUBE_CPUS ?= 6
SKIP_UIS ?= false
E2E_KUBECONTEXT ?= $(MINIKUBE_PROFILE)
KC := kubectl --context=$(MINIKUBE_PROFILE)
LOCAL_KUBE_CONTEXT ?=
# Exact selectors with an established deployment route in scripts/minikube/dev.sh.
# Keep this public Make boundary aligned with build-images.sh so typos fail
# before a lock is acquired or any image/deployment mutation begins.
MINIKUBE_DEPLOY_SERVICE_SELECTORS := control-api control-ui external-rest-api hcc mcp-host profile-ui rpc-proxy
MINIKUBE_DEPLOY_SERVICE := $(strip $(SVC))
MINIKUBE_DEPLOY_SERVICE_SUPPORTED := $(and $(filter 1,$(words $(MINIKUBE_DEPLOY_SERVICE))),$(filter $(MINIKUBE_DEPLOY_SERVICE_SELECTORS),$(MINIKUBE_DEPLOY_SERVICE)))
MINIKUBE_DEPLOY_NAMESPACE := $(strip $(NS))
minikube_deployment = $(if $(filter mcp-host,$(strip $(1))),chatllm,$(strip $(1)))
DEPLOYMENT ?= $(call minikube_deployment,$(MINIKUBE_DEPLOY_SERVICE))
MINIKUBE_EFFECTIVE_DEPLOYMENT := $(or $(strip $(DEPLOYMENT)),$(call minikube_deployment,$(MINIKUBE_DEPLOY_SERVICE)))
.PHONY: minikube-start
minikube-start: ## Start minikube cluster (starts Docker Desktop if needed)
@if ! scripts/minikube/docker-cli-env.sh --check-info; then \
echo "Starting Docker Desktop..."; \
open -a "Docker Desktop" 2>/dev/null || open -a Docker 2>/dev/null || true; \
echo "Waiting for Docker daemon..."; \
docker_start_timeout="$${MINIKUBE_DOCKER_START_TIMEOUT_SECONDS:-60}"; \
MINIKUBE_DOCKER_START_TIMEOUT_SECONDS="$$docker_start_timeout" \
scripts/minikube/docker-cli-env.sh --wait-for-info || { \
echo "ERROR: Docker not available after $${docker_start_timeout}s"; \
exit 1; \
}; \
echo "Docker ready."; \
fi
MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" MINIKUBE_MULTI_NODE="$(MINIKUBE_MULTI_NODE)" MINIKUBE_NODES="$(MINIKUBE_NODES)" MINIKUBE_MEMORY="$(MINIKUBE_MEMORY)" MINIKUBE_CPUS="$(MINIKUBE_CPUS)" scripts/minikube/start.sh
@$(MAKE) --no-print-directory MINIKUBE_STARTUP_AUTH_SYNC_MODE=shared-profile-mcp minikube-sync-auth-key-if-present
.PHONY: minikube-stop
minikube-stop: ## Stop minikube cluster
minikube stop -p $(MINIKUBE_PROFILE)
# Images come from ghcr by default: `make minikube-setup` on a clean clone
# pulls ~25 published images instead of building 28 from source. Build
# everything locally with `make minikube-setup-local` (or IMAGE_SOURCE=local).
IMAGE_SOURCE ?= ghcr
# GFS Secret/role mutation is owned by the canonical T2 lease. Callers that
# intentionally run the full T2 transition set this to true and pass the
# inherited opaque T2_LOCK_TOKEN; ordinary deploys render/filter GFS resources.
MINIKUBE_GFS_MUTATION ?= false
.PHONY: minikube-setup
minikube-setup: ## Clean install from scratch, PULLING published images (IMAGE_SOURCE=local or `make minikube-setup-local` builds instead). Rebuilds the DB; REUSE_DB=true keeps it. SKIP_UIS=true omits Control/Profile UI. Runs 'prereqs' first (SKIP_PREREQS=true to bypass). Needs ADMIN_PASSWORD in .env.
@if [ "$(SKIP_PREREQS)" != "true" ]; then $(MAKE) --no-print-directory prereqs; fi
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
MINIKUBE_SKIP_UIS="$(SKIP_UIS)" MINIKUBE_SEED_PROFILE="$(SEED_PROFILE)" REUSE_DB="$(REUSE_DB)" \
IMAGE_SOURCE="$(IMAGE_SOURCE)" MINIKUBE_IMAGE_TAG="$(MINIKUBE_IMAGE_TAG)" \
scripts/minikube/full-setup.sh $(ARGS)
.PHONY: minikube-setup-local
minikube-setup-local: ## Clean install building every image from source (the pre-2026-08 behaviour; ~20 min on a clean clone). Use when you are changing service code, or when no published image exists for your platform.
@$(MAKE) --no-print-directory minikube-setup IMAGE_SOURCE=local
.PHONY: minikube-setup-e2e
minikube-setup-e2e: ## Full setup + E2E fixtures (test user, e2e-* recipes, demo MCP servers). Pulls published images, then builds the two unpublished E2E coordinator fixtures.
@$(MAKE) --no-print-directory minikube-setup SEED_PROFILE=e2e
@if [ "$(IMAGE_SOURCE)" = "ghcr" ]; then \
echo "Building the two unpublished E2E coordinator fixtures..."; \
$(MAKE) --no-print-directory minikube-build-e2e-fixtures; \
fi
.PHONY: minikube-teardown
minikube-teardown: ## Remove deployments (keep namespaces/CRDs)
@scripts/minikube/teardown.sh
.PHONY: minikube-pull-images minikube-pull-images-body
minikube-pull-images: ## Pull ALL published images into minikube at the pinned release tag (MINIKUBE_IMAGE_TAG=<tag> overrides the pin for this run only)
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-pull-images-body
minikube-pull-images-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" MINIKUBE_IMAGE_TAG="$(MINIKUBE_IMAGE_TAG)" \
CONTROL_API_REAL_PG_CONTEXT="$(MINIKUBE_PROFILE)" \
scripts/minikube/pull-images.sh
.PHONY: minikube-build-images minikube-build-images-body
minikube-build-images: ## Build and load ALL Docker images into minikube (with SHA verification)
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-build-images-body
minikube-build-images-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh
.PHONY: minikube-build-custom-coordinator-fixture minikube-build-custom-coordinator-fixture-body
minikube-build-custom-coordinator-fixture: ## Build only the custom coordinator E2E fixture image in minikube
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-build-custom-coordinator-fixture-body
minikube-build-custom-coordinator-fixture-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=workflow-custom-sdk-e2e
.PHONY: minikube-build-e2e-fixtures minikube-build-e2e-fixtures-body
.PHONY: minikube-install-627-real-tools
minikube-install-627-real-tools: ## Install real Worktracker and Wikipedia baseline under the owned profile lease
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- node scripts/e2e/install-627-real-tools.mjs
.PHONY: minikube-build-627-worktracker
minikube-build-627-worktracker: ## Build the reviewed Worktracker sources locally without registry credentials or publication
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- bash scripts/e2e/build-627-worktracker.sh
.PHONY: minikube-build-image-capabilities-fixture minikube-build-image-capabilities-fixture-body
minikube-build-image-capabilities-fixture: ## Build the unpublished image-input provider fixture under the owned profile lease
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-build-image-capabilities-fixture-body
minikube-build-image-capabilities-fixture-body:
@bash scripts/minikube/require-t2-mutation-lock.sh
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=mcp-host
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=image-capabilities-mcp-host
.PHONY: minikube-run-image-capabilities minikube-restore-image-capabilities
minikube-run-image-capabilities: ## Run the visible image journey with an isolated external-provider fixture and restore the Host
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- node scripts/e2e/image-capabilities-fixture.mjs run
minikube-restore-image-capabilities: ## Resume restoration of a recorded image fixture run (IMAGE_CAPABILITIES_RUN_DIR=<dir printed by minikube-run-image-capabilities>)
@test -n "$(IMAGE_CAPABILITIES_RUN_DIR)" || { echo "IMAGE_CAPABILITIES_RUN_DIR is required: the run directory printed by 'make minikube-run-image-capabilities'"; exit 1; }
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
IMAGE_CAPABILITIES_RUN_DIR="$(IMAGE_CAPABILITIES_RUN_DIR)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- node scripts/e2e/image-capabilities-fixture.mjs restore
.PHONY: minikube-build-codex-approved-tools-fixtures minikube-build-codex-approved-tools-fixtures-body
minikube-build-codex-approved-tools-fixtures: ## Acquire optional Codex tools fixture images before T2 reconcile
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-build-codex-approved-tools-fixtures-body
minikube-build-codex-approved-tools-fixtures-body:
@bash scripts/minikube/require-t2-mutation-lock.sh
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=control-api
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=codex-approved-tools-control-api-e2e
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=codex-llm-proxy
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=codex-approved-tools-proxy-e2e
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=codex-approved-tools-mcp-e2e
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=workflow-custom-sdk-e2e
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=codex-approved-tools-workflow-e2e
.PHONY: minikube-prepare-codex-approved-tools minikube-run-codex-approved-tools minikube-restore-codex-approved-tools
minikube-prepare-codex-approved-tools: ## Prepare isolated deterministic tools fixtures; requires prior image acquisition and fresh run directory
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- node scripts/e2e/prepare-codex-approved-tools.mjs prepare
minikube-run-codex-approved-tools: ## Prepare, run visible deterministic E2E and restore production proxy image in owned Minikube
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- node scripts/e2e/prepare-codex-approved-tools.mjs run
minikube-restore-codex-approved-tools: ## Restore recorded proxy image/env and close only this fixture run's owned forwards
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- node scripts/e2e/prepare-codex-approved-tools.mjs restore
minikube-build-e2e-fixtures: ## Build the two unpublished coordinator E2E fixtures under one mutation lease
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-build-e2e-fixtures-body
minikube-build-e2e-fixtures-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=workflow-custom-sdk-e2e
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=workflow-plugin-sdk-e2e
.PHONY: minikube-verify-images
minikube-verify-images: ## Verify every image the cluster runs is present. The mode comes from deploy/minikube/.image-manifest.json (what was actually built/pulled), not from IMAGE_SOURCE; SEED_PROFILE=e2e also checks the two E2E fixtures.
@IMAGE_SOURCE="$(IMAGE_SOURCE)" MINIKUBE_IMAGE_TAG="$(MINIKUBE_IMAGE_TAG)" \
MINIKUBE_SEED_PROFILE="$(SEED_PROFILE)" \
scripts/minikube/build-images.sh --verify-only
.PHONY: minikube-verify
minikube-verify: ## Verify all McpServers have resolved envSecrets (standalone smoke check)
@KUBE_CONTEXT=$(MINIKUBE_PROFILE) bash scripts/minikube/verify-mcpserver-secrets.sh
.PHONY: minikube-verify-gfs
minikube-verify-gfs: ## Verify gfs permission-store wiring (Secret DSN populated, gfsc rolled after rotation, /readyz green)
@CONTEXT=$(MINIKUBE_PROFILE) bash scripts/minikube/verify-gfs.sh
# ── Minikube Deploy ────────────────────────────────────────────────────
# Individual stack deploys removed — use minikube-deploy-all or minikube-setup.
# The old targets (minikube-deploy-core, -mcp, -profiles, -channels, -ui)
# only applied ConfigMaps, not actual deployments, and were misleading.
.PHONY: minikube-deploy-instances
minikube-deploy-instances: ## Apply CRD test instances (context, host, channel)
$(KC) apply -f deploy/overlays/minikube/instances/
.PHONY: minikube-detect-k8s-api-ip
minikube-detect-k8s-api-ip: ## Patch overlays/minikube/patches/k8s-api-ip.yaml with current node IP
@CONTEXT=$(MINIKUBE_PROFILE) deploy/scripts/minikube-detect-k8s-api-ip.sh
.PHONY: minikube-deploy-all minikube-deploy-all-body
minikube-deploy-all: ## Deploy ALL services via Kustomize minikube overlay
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
MINIKUBE_GFS_MUTATION="$(MINIKUBE_GFS_MUTATION)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-deploy-all-body
minikube-deploy-all-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
@$(MAKE) --no-print-directory minikube-detect-k8s-api-ip
@# Upgrade path: adopt/validate writer and stage reader before HCC cutover.
@if [ "$(MINIKUBE_GFS_MUTATION)" != "true" ]; then echo "[minikube-deploy-all] GFS mutation disabled for this non-T2 sync"; fi
@if [ "$(MINIKUBE_GFS_MUTATION)" = "true" ]; then \
CONTEXT=$(MINIKUBE_PROFILE) bash deploy/scripts/apply-gfs-writer-secret.sh; \
writer_dsn="$$(kubectl --context=$(MINIKUBE_PROFILE) -n gfs get secret gfs-controller-db -o 'jsonpath={.data.connection-string}')" || { \
echo "[minikube-deploy-all] failed to classify the existing GFS writer Secret; refusing HCC cutover" >&2; exit 1; \
}; \
if [[ -n "$$writer_dsn" ]]; then \
kubectl --context=$(MINIKUBE_PROFILE) -n control-plane rollout status deployment/control-api --timeout=5s >/dev/null 2>&1 || { \
echo "[minikube-deploy-all] existing GFS writer detected but control-api is not Ready; refusing HCC cutover" >&2; exit 1; \
}; \
T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" CONTEXT=$(MINIKUBE_PROFILE) T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" bash deploy/scripts/reconcile-gfs-deploy-credentials.sh; \
else \
echo "[minikube-deploy-all] fresh bootstrap: reader staging deferred until post-migration full-setup (GFSC fail-closed)"; \
fi; \
fi
@# THE OVERLAY FOLLOWS THE CLUSTER, NOT THIS SHELL. Hardcoding
@# deploy/overlays/minikube applied clerum/*:test image refs to a cluster
@# that pulled ghcr release images: nothing ever built those tags there, so
@# a deploy/*-only change produced cluster-wide ImagePullBackOff, and a
@# pre-gate full-deployment silently flipped a ghcr cluster to local refs
@# with no record. image-mode.sh resolves it from what the last image
@# acquisition recorded. It runs HERE, after minikube-detect-k8s-api-ip
@# above, because an overridden tag renders from a copy of deploy/ that must
@# already contain the generated k8s-api-ip.yaml.
@set -o pipefail; \
render_dir="$$(bash scripts/minikube/image-mode.sh --render-dir)" && \
if [ "$(MINIKUBE_GFS_MUTATION)" = "true" ]; then \
kubectl --context=$(MINIKUBE_PROFILE) kustomize "$$render_dir" | kubectl --context=$(MINIKUBE_PROFILE) apply -f -; \
else \
filtered_manifest="$$(mktemp "$${TMPDIR:-/tmp}/evenfire-gfs-filter.XXXXXX")"; \
trap 'rm -f -- "$$filtered_manifest"' EXIT; \
if ! kubectl --context=$(MINIKUBE_PROFILE) kustomize "$$render_dir" | python3 scripts/minikube/filter-gfs-resources.py >"$$filtered_manifest"; then \
echo "[minikube-deploy-all] failed to render or filter the non-GFS overlay" >&2; exit 1; \
fi; \
if [ -s "$$filtered_manifest" ]; then \
kubectl --context=$(MINIKUBE_PROFILE) apply -f "$$filtered_manifest"; \
else \
echo "[minikube-deploy-all] filtered overlay contains no non-GFS resources; skipping apply"; \
fi; \
fi
CONTEXT=$(MINIKUBE_PROFILE) bash deploy/scripts/apply-inter-service-tokens.sh
@if [ "$(MINIKUBE_GFS_MUTATION)" = "true" ]; then \
$(KC) apply -f deploy/overlays/minikube/instances/; \
else \
filtered_manifest="$$(mktemp "$${TMPDIR:-/tmp}/evenfire-gfs-instances-filter.XXXXXX")"; \
trap 'rm -f -- "$$filtered_manifest"' EXIT; \
if ! python3 scripts/minikube/filter-gfs-resources.py deploy/overlays/minikube/instances/*.yaml >"$$filtered_manifest"; then \
echo "[minikube-deploy-all] failed to filter the non-GFS instance resources" >&2; exit 1; \
fi; \
if [ -s "$$filtered_manifest" ]; then \
$(KC) apply -f "$$filtered_manifest"; \
else \
echo "[minikube-deploy-all] filtered instances contain no non-GFS resources; skipping apply"; \
fi; \
fi
@# Kustomize reapplies the persisted mcp-host ConfigMap, which can overwrite
@# CLERUM_AUTH_JWT_PUBLIC_KEY with an older repo value. Always re-sync from
@# rpc-proxy-secrets after each full overlay apply so Desktop/rpc-proxy/mcp-host
@# stay on the same JWT validation key.
@MINIKUBE_GFS_MUTATION="$(MINIKUBE_GFS_MUTATION)" $(MAKE) --no-print-directory minikube-sync-auth-key
@# The pre-overlay helper migrates legacy last-applied ownership without
@# removing the provisioning-owned connection-string. When the GFS stack is
@# deployed AND control-api is Ready (migration 0048 applied), re-provision
@# the gfs_controller DSN so gfsc never runs with an empty or stale
@# credential (issue #775). Fails loud if provisioning itself fails. When
@# control-api is not Ready yet (fresh cluster mid-setup), provisioning is
@# deferred LOUDLY to the full-setup/pre-gate-sync flow that already orders
@# it after control-api migrations.
@if [ "$(MINIKUBE_GFS_MUTATION)" = "true" ]; then \
gfs_config_probe="$$(kubectl --context=$(MINIKUBE_PROFILE) get configmap gfs-config -n gfs 2>&1)" || { \
if [[ "$$gfs_config_probe" == *NotFound* || "$$gfs_config_probe" == *"not found"* ]]; then \
echo "[minikube-deploy-all] GFS is not deployed; skipping post-overlay credential reconciliation"; \
gfs_config_probe=""; \
else \
echo "[minikube-deploy-all] unable to inspect GFS configmap; refusing to continue: $$gfs_config_probe" >&2; exit 1; \
fi; \
}; \
if [ -n "$$gfs_config_probe" ]; then \
if kubectl --context=$(MINIKUBE_PROFILE) -n control-plane rollout status deployment/control-api --timeout=5s >/dev/null 2>&1; then \
CONTEXT=$(MINIKUBE_PROFILE) T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" bash deploy/scripts/wait-gfsc-secret-references.sh; \
T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" CONTEXT=$(MINIKUBE_PROFILE) T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" bash deploy/scripts/reconcile-gfs-deploy-credentials.sh; \
else \
echo "[minikube-deploy-all] control-api not Ready — gfs DSN provisioning DEFERRED to full-setup/pre-gate-sync ordering (gfsc stays fail-closed until then)"; \
fi; \
fi; \
else \
echo "[minikube-deploy-all] skipping post-overlay GFS credential reconciliation"; \
fi
.PHONY: minikube-verify-networkpolicies
minikube-verify-networkpolicies: ## Verify rendered minikube NetworkPolicies exist in cluster
@profile_cache="$${HOME}/.cache/clerum/minikube-profiles/$(MINIKUBE_PROFILE)"; \
if [ -f "$$profile_cache/deploy/scripts/verify-networkpolicies.sh" ]; then \
echo "Using branch-profile cached overlay: $$profile_cache/deploy"; \
bash "$$profile_cache/deploy/scripts/verify-networkpolicies.sh" --overlay minikube --context $(MINIKUBE_PROFILE); \
else \
echo "Using worktree overlay: deploy"; \
bash deploy/scripts/verify-networkpolicies.sh --overlay minikube --context $(MINIKUBE_PROFILE); \
fi
.PHONY: minikube-restart-all
minikube-restart-all: ## Restart all Clerum deployments
@for ns in control-plane mcp-host mcp-server profiles rpc-proxy channels; do \
$(KC) rollout restart deploy -n $$ns 2>/dev/null || true; \
done
@echo "All deployments restarted."
.PHONY: minikube-deploy-crds minikube-deploy-crds-body
minikube-deploy-crds: ## Install/upgrade CRDs via Helm chart + apply CRD YAML (idempotent)
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
MINIKUBE_GFS_MUTATION="$(MINIKUBE_GFS_MUTATION)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-deploy-crds-body
minikube-deploy-crds-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
kubectl --context=$(MINIKUBE_PROFILE) apply -f deploy/base/namespaces.yaml
@if [ "$(MINIKUBE_GFS_MUTATION)" != "true" ]; then \
echo "[minikube-deploy-crds] GFS CRD mutation disabled for this non-T2 gate"; \
helm upgrade --install --skip-crds --kube-context=$(MINIKUBE_PROFILE) clerum-crds ./charts/clerum-crds; \
for crd in ./charts/clerum-crds/crds/*.yaml; do \
case "$$crd" in *globalfilesystem.yaml) continue ;; esac; \
kubectl --context=$(MINIKUBE_PROFILE) apply -f "$$crd"; \
done; \
else \
helm upgrade --install --kube-context=$(MINIKUBE_PROFILE) clerum-crds ./charts/clerum-crds; \
kubectl --context=$(MINIKUBE_PROFILE) apply -f ./charts/clerum-crds/crds/; \
fi
.PHONY: minikube-deploy-service minikube-deploy-service-body
minikube-deploy-service: ## Rebuild single image + rollout restart deployment (usage: make minikube-deploy-service SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm])
@$(if $(MINIKUBE_DEPLOY_SERVICE),:,echo "ERROR: SVC required. Usage: make minikube-deploy-service SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm]"; exit 1)
@$(if $(MINIKUBE_DEPLOY_SERVICE_SUPPORTED),:,echo "ERROR: unsupported SVC selector. Supported: $(MINIKUBE_DEPLOY_SERVICE_SELECTORS)"; exit 1)
@$(if $(MINIKUBE_DEPLOY_NAMESPACE),:,echo "ERROR: NS required. Usage: make minikube-deploy-service SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm]"; exit 1)
@$(if $(MINIKUBE_EFFECTIVE_DEPLOYMENT),:,echo "ERROR: effective DEPLOYMENT could not be resolved from SVC"; exit 1)
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-deploy-service-body
minikube-deploy-service-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
@$(if $(MINIKUBE_DEPLOY_SERVICE),:,echo "ERROR: SVC required. Usage: make minikube-deploy-service SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm]"; exit 1)
@$(if $(MINIKUBE_DEPLOY_SERVICE_SUPPORTED),:,echo "ERROR: unsupported SVC selector. Supported: $(MINIKUBE_DEPLOY_SERVICE_SELECTORS)"; exit 1)
@$(if $(MINIKUBE_DEPLOY_NAMESPACE),:,echo "ERROR: NS required. Usage: make minikube-deploy-service SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm]"; exit 1)
@$(if $(MINIKUBE_EFFECTIVE_DEPLOYMENT),:,echo "ERROR: effective DEPLOYMENT could not be resolved from SVC"; exit 1)
@echo "Deploying image selector $(MINIKUBE_DEPLOY_SERVICE) to deployment/$(MINIKUBE_EFFECTIVE_DEPLOYMENT) in namespace $(MINIKUBE_DEPLOY_NAMESPACE)"
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" scripts/minikube/build-images.sh --only=$(MINIKUBE_DEPLOY_SERVICE)
kubectl --context=$(MINIKUBE_PROFILE) -n $(MINIKUBE_DEPLOY_NAMESPACE) rollout restart deployment/$(MINIKUBE_EFFECTIVE_DEPLOYMENT)
kubectl --context=$(MINIKUBE_PROFILE) -n $(MINIKUBE_DEPLOY_NAMESPACE) rollout status deployment/$(MINIKUBE_EFFECTIVE_DEPLOYMENT) --timeout=180s
.PHONY: minikube-restart-deploy minikube-restart-deploy-body
minikube-restart-deploy: ## Restart a single deployment without rebuilding (usage: make minikube-restart-deploy SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm])
@$(if $(MINIKUBE_DEPLOY_SERVICE),:,echo "ERROR: SVC required. Usage: make minikube-restart-deploy SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm]"; exit 1)
@$(if $(MINIKUBE_DEPLOY_SERVICE_SUPPORTED),:,echo "ERROR: unsupported SVC selector. Supported: $(MINIKUBE_DEPLOY_SERVICE_SELECTORS)"; exit 1)
@$(if $(MINIKUBE_DEPLOY_NAMESPACE),:,echo "ERROR: NS required. Usage: make minikube-restart-deploy SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm]"; exit 1)
@$(if $(MINIKUBE_EFFECTIVE_DEPLOYMENT),:,echo "ERROR: effective DEPLOYMENT could not be resolved from SVC"; exit 1)
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-restart-deploy-body
minikube-restart-deploy-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
@$(if $(MINIKUBE_DEPLOY_SERVICE),:,echo "ERROR: SVC required. Usage: make minikube-restart-deploy SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm]"; exit 1)
@$(if $(MINIKUBE_DEPLOY_SERVICE_SUPPORTED),:,echo "ERROR: unsupported SVC selector. Supported: $(MINIKUBE_DEPLOY_SERVICE_SELECTORS)"; exit 1)
@$(if $(MINIKUBE_DEPLOY_NAMESPACE),:,echo "ERROR: NS required. Usage: make minikube-restart-deploy SVC=mcp-host NS=mcp-host [DEPLOYMENT=chatllm]"; exit 1)
@$(if $(MINIKUBE_EFFECTIVE_DEPLOYMENT),:,echo "ERROR: effective DEPLOYMENT could not be resolved from SVC"; exit 1)
@echo "Restarting deployment/$(MINIKUBE_EFFECTIVE_DEPLOYMENT) in namespace $(MINIKUBE_DEPLOY_NAMESPACE)"
kubectl --context=$(MINIKUBE_PROFILE) -n $(MINIKUBE_DEPLOY_NAMESPACE) rollout restart deployment/$(MINIKUBE_EFFECTIVE_DEPLOYMENT)
kubectl --context=$(MINIKUBE_PROFILE) -n $(MINIKUBE_DEPLOY_NAMESPACE) rollout status deployment/$(MINIKUBE_EFFECTIVE_DEPLOYMENT) --timeout=180s
# ── Minikube Secrets & Keys ─────────────────────────────────────────
#
# KEY INVARIANT: After generating new keys, the public key must be in sync across:
# 1. rpc-proxy-secrets (namespace: rpc-proxy) → RPC_PROXY_JWT_PUBLIC_KEY
# 2. mcp-host-config (namespace: mcp-host) → CLERUM_AUTH_JWT_PUBLIC_KEY
# 3. gfs-config (namespace: gfs) → jwt-public-key
# 4. deploy/overlays/minikube/configmaps/mcp-host-config.yaml (persisted in repo)
#
# Use `make minikube-sync-auth-key` to copy the public key automatically
# from rpc-proxy-secrets into both runtime ConfigMaps after key regeneration.
#
.PHONY: minikube-gen-keys minikube-gen-keys-body
minikube-gen-keys: ## Generate JWT signing keys + auto-sync to mcp-host-config
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-gen-keys-body
minikube-gen-keys-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
@scripts/minikube/generate-keys.sh
@if [ -f deploy/minikube/secrets/jwt-signing-keys.yaml ]; then \
$(KC) apply -f deploy/minikube/secrets/jwt-signing-keys.yaml; \
else \
echo "JWT signing key manifest not present; using existing cluster keys."; \
fi
@echo "Syncing auth key..."
@$(MAKE) --no-print-directory minikube-sync-auth-key
.PHONY: minikube-apply-secrets
minikube-apply-secrets: ## Apply all secrets to cluster (LLM keys read from .env if present)
@# Apply JWT signing keys ONLY if they don't already exist (anti-pattern: regenerating
@# keys invalidates all tokens and breaks admin login). Use FORCE_REGEN=true to override.
@if ! $(KC) get secret control-api-secrets -n control-plane >/dev/null 2>&1; then \
echo "Creating JWT signing keys (first time)..."; \
if [ ! -f deploy/minikube/secrets/jwt-signing-keys.yaml ]; then \
scripts/minikube/generate-keys.sh; \
fi; \
$(KC) apply -f deploy/minikube/secrets/jwt-signing-keys.yaml; \
else \
echo "JWT signing keys already exist — skipping (use FORCE_REGEN=true make minikube-gen-keys to regenerate)"; \
fi
$(KC) apply -f deploy/overlays/minikube/secrets/inter-service-tokens.yaml
@channel_file="$(ls deploy/overlays/minikube/secrets/channel-*.yaml 2>/dev/null | head -n 1)"; \
if [ -n "$$channel_file" ]; then \
$(KC) apply -f "$$channel_file"; \
else \
echo "Channel file not present; per-Host channel values are managed through Control UI/control-api."; \
fi
@# LLM API keys — all 22 providers from the registry; reads the main checkout
@# .env when running from a worktree. Original four keep placeholder fallbacks.
@CONTEXT=$(MINIKUBE_PROFILE) bash scripts/minikube/apply-llm-secret.sh
.PHONY: minikube-apply-namespaces
minikube-apply-namespaces: ## Create all namespaces
$(KC) apply -f deploy/base/namespaces.yaml
.PHONY: minikube-sync-auth-key minikube-sync-auth-key-body minikube-sync-auth-key-shared-profile
minikube-sync-auth-key: ## Sync JWT public key from rpc-proxy-secrets into runtime ConfigMaps when drift exists
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
MINIKUBE_GFS_MUTATION="$(MINIKUBE_GFS_MUTATION)" \
bash scripts/minikube/with-t2-mutation-lock.sh -- \
$(MAKE) --no-print-directory minikube-sync-auth-key-body
minikube-sync-auth-key-body:
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
T2_SKIP_LOCK=true T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/require-t2-mutation-lock.sh
@if [ "$(MINIKUBE_GFS_MUTATION)" = "true" ]; then \
T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/sync-auth-key.sh --context=$(MINIKUBE_PROFILE); \
else \
T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" T2_SKIP_LOCK="$(T2_SKIP_LOCK)" T2_LOCK_TOKEN="$(T2_LOCK_TOKEN)" \
bash scripts/minikube/sync-auth-key.sh --context=$(MINIKUBE_PROFILE) --skip-gfs --require-mcp; \
fi
minikube-sync-auth-key-shared-profile: ## Sync only MCP auth on the documented shared profile during startup
@T2_PROJECT_DIR="$(CURDIR)" T2_PROFILE="$(MINIKUBE_PROFILE)" T2_CONTEXT="$(MINIKUBE_PROFILE)" \
bash scripts/minikube/sync-auth-key.sh --context=$(MINIKUBE_PROFILE) --shared-profile-bootstrap --skip-gfs --require-mcp
.PHONY: minikube-sync-auth-key-if-present
minikube-sync-auth-key-if-present: ## Sync JWT public key only when minikube auth resources already exist
@kubectl_probe_is_not_found() { \
probe_output="$$1"; \
probe_kind="$$2"; \
probe_name="$$3"; \
[[ "$$probe_output" =~ ^Error[[:space:]]+from[[:space:]]+server[[:space:]]+\(NotFound\):[[:space:]] ]] || return 1; \
probe_detail="$${probe_output#*): }"; \
case "$$probe_kind" in \
secret) case "$$probe_detail" in secret\ *|secrets\ *) ;; *) return 1 ;; esac ;; \
configmap) case "$$probe_detail" in configmap\ *|configmaps\ *) ;; *) return 1 ;; esac ;; \
*) return 1 ;; \
esac; \
[[ "$$probe_detail" == *"\"$$probe_name\""* ]] || return 1; \
}; \
rpc_probe_status=0; \
rpc_probe_output="$$( $(KC) get secret rpc-proxy-secrets -n rpc-proxy 2>&1 )" || rpc_probe_status=$$?; \
if [ "$$rpc_probe_status" -ne 0 ]; then \
if kubectl_probe_is_not_found "$$rpc_probe_output" secret rpc-proxy-secrets; then \
echo "Skipping auth key sync (rpc-proxy-secrets not found yet)."; exit 0; \
fi; \
printf '%s\n' "$$rpc_probe_output" >&2; exit "$$rpc_probe_status"; \
fi; \
mcp_probe_status=0; \
mcp_probe_output="$$( $(KC) get configmap mcp-host-config -n mcp-host 2>&1 )" || mcp_probe_status=$$?; \
if [ "$$mcp_probe_status" -ne 0 ]; then \
if kubectl_probe_is_not_found "$$mcp_probe_output" configmap mcp-host-config; then \
echo "Skipping auth key sync (mcp-host-config not found yet)."; exit 0; \
fi; \
printf '%s\n' "$$mcp_probe_output" >&2; exit "$$mcp_probe_status"; \
fi; \
if [ "$(MINIKUBE_STARTUP_AUTH_SYNC_MODE)" = "shared-profile-mcp" ]; then \
$(MAKE) --no-print-directory minikube-sync-auth-key-shared-profile; \
elif [ "$(T2_MUTATION_LOCK_WRAPPED)" = "true" ]; then \
$(MAKE) --no-print-directory minikube-sync-auth-key-body; \
else \
$(MAKE) --no-print-directory minikube-sync-auth-key; \
fi
.PHONY: minikube-sync-codex-subscription-url
minikube-sync-codex-subscription-url: ## Resolve branch Control UI URL and sync CONTROL_API_CONTROL_UI_BASE_URL for Codex OAuth
@bash scripts/minikube/sync-codex-subscription-control-ui-url.sh --context=$(MINIKUBE_PROFILE)
# ── Minikube Port Forwards ──────────────────────────────────────────
.PHONY: minikube-pf-control-ui
minikube-pf-control-ui: ## Port-forward Control UI → localhost:3000
$(KC) port-forward svc/control-ui -n control-plane 3000:3000
.PHONY: minikube-pf-control-api
minikube-pf-control-api: ## Port-forward Control API → localhost:8090
scripts/dev/resilient-kubectl-port-forward.sh "$(MINIKUBE_PROFILE)" control-plane control-api 8090 8090
.PHONY: minikube-pf-external-api
minikube-pf-external-api: ## Port-forward External REST API → localhost:8091
scripts/dev/resilient-kubectl-port-forward.sh "$(MINIKUBE_PROFILE)" profiles external-rest-api 8091 8091
.PHONY: minikube-pf-rpc-proxy
minikube-pf-rpc-proxy: ## Port-forward RPC Proxy → localhost:8094
scripts/dev/resilient-kubectl-port-forward.sh "$(MINIKUBE_PROFILE)" rpc-proxy rpc-proxy 8094 8094
.PHONY: minikube-pf-mcp-host
minikube-pf-mcp-host: ## Port-forward MCP Host → localhost:8080
$(KC) port-forward svc/mcp-host -n mcp-host 8080:8080
.PHONY: minikube-pf-desktop
minikube-pf-desktop: ## Port-forward all services needed by Desktop App (background)
@echo "Starting port-forwards for Desktop App..."
@$(KC) port-forward svc/control-api -n control-plane 8090:8090 &
@$(KC) port-forward svc/external-rest-api -n profiles 8091:8091 &
@$(KC) port-forward svc/rpc-proxy -n rpc-proxy 8094:8094 &
@echo "Desktop App ready: control-api=:8090 external-rest-api=:8091 rpc-proxy=:8094"
@echo " Recipe Manager needs CONTROL_API_ADMIN_USERNAME + CONTROL_API_ADMIN_PASSWORD"
@echo "Press Ctrl+C to stop all port-forwards"
@wait
.PHONY: minikube-pf-all
minikube-pf-all: ## Port-forward ALL services (Control UI + Desktop App)
@scripts/minikube/pf-all-stack.sh --hold
.PHONY: minikube-pf-all-bg
minikube-pf-all-bg: ## Refresh background port-forwards for gate automation
@scripts/minikube/pf-all-stack.sh
.PHONY: minikube-pre-gate-sync
minikube-pre-gate-sync: ## Enforce minikube sync before a gate (use GATE=<name>)
@scripts/minikube/pre-gate-sync.sh --gate "$${GATE:-manual}" $(ARGS)
.PHONY: test-gfs-real-postgres-minikube
test-gfs-real-postgres-minikube: ## Run GFS T1 real-Postgres suites against a validated branch-owned Minikube profile
@CONTEXT="$(MINIKUBE_PROFILE)" bash scripts/e2e/gfs-real-pg-minikube-gate.sh
.PHONY: minikube-t2-preflight
minikube-t2-preflight: ## Read-only readiness planner (not T0/T1/T2); fail-loud on an unbootstrapped profile
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" CONTROL_API_REAL_PG_CONTEXT="$(CONTROL_API_REAL_PG_CONTEXT)" \
scripts/minikube/t2-preflight.sh
.PHONY: minikube-t2
minikube-t2: ## Full orchestrator: T0, Real PostgreSQL T1, then exact-head T2
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" CONTROL_API_REAL_PG_CONTEXT="$(CONTROL_API_REAL_PG_CONTEXT)" \
scripts/minikube/t2.sh
.PHONY: minikube-t2-np08-hcc-authorization
minikube-t2-np08-hcc-authorization: minikube-t2 ## Run canonical T2 including the required deployed NP-08 Host-to-HCC authorization journey
# Dedicated #604 certification: the bounded health lane runs the real
# policy lifecycle, business invocation and three watch reconnections.
.PHONY: minikube-t2-hcc-networkpolicy-lifecycle
minikube-t2-hcc-networkpolicy-lifecycle: ## Run canonical T2 with the HCC NetworkPolicy lifecycle and watch-reconnection health gate
@T2_HEALTHCHECK_COMMAND='bash scripts/e2e/e2e-hcc-networkpolicy-lifecycle.sh' \
T2_HEALTHCHECK_TIMEOUT_SECONDS=900 T2_HEALTHCHECK_KILL_GRACE_SECONDS=300 $(MAKE) minikube-t2
.PHONY: minikube-t2-hcc-watch-recovery
minikube-t2-hcc-watch-recovery: ## Certify PR A recovery omission, runtime repair and API-gate recovery in owned Minikube
@bash scripts/tests/test-hcc-watch-api-proxy.sh
@bash scripts/tests/test-hcc-watch-pr-a.sh
@E2E_HCC_PR_A=1 $(MAKE) minikube-t2-hcc-networkpolicy-lifecycle
.PHONY: minikube-t2-runtime
minikube-t2-runtime: ## Exact-head T2 after T0 and T1 already passed on this HEAD and profile
@T2_RUN_T0=false T2_RUN_T1=false \
MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" CONTROL_API_REAL_PG_CONTEXT="$(CONTROL_API_REAL_PG_CONTEXT)" \
scripts/minikube/t2.sh
.PHONY: minikube-t2-real-postgres
minikube-t2-real-postgres: ## Run the explicit local Real PostgreSQL lane without changing CI's DSN contract
@MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" CONTROL_API_REAL_PG_CONTEXT="$(CONTROL_API_REAL_PG_CONTEXT)" \
scripts/e2e/minikube-real-postgres.sh
.PHONY: minikube-t2-public-boundary
minikube-t2-public-boundary: ## Reject secrets, credentials, private URLs, and raw runtime artifacts from the public diff
@scripts/tests/test-minikube-t2-public-boundary.sh
.PHONY: minikube-t2-scenarios
minikube-t2-scenarios: ## Exercise the fail-loud negative cases and transition classifier without a cluster
@scripts/tests/test-minikube-t2-scenarios.sh
.PHONY: minikube-verify-network-policy
minikube-verify-network-policy: ## Prove NetworkPolicy enforcement in clerum-test/minikube before custom-image gates
@CONTEXT="$(MINIKUBE_PROFILE)" scripts/minikube/verify-network-policy-enforcement.sh
# E2E_CONTEXT drives which cluster desktop-app Playwright runs against.
# Only these are permitted; "gke_your-gcp-project_us-central1-a_clerum" (prod) is hard-blocked.
E2E_CONTEXT ?= clerum-test
E2E_DESKTOP_ALLOWED_CONTEXTS := clerum-test gke_your-gcp-project_us-central1-a_example-dev
E2E_PROD_CONTEXT := gke_your-gcp-project_us-central1-a_clerum
.PHONY: e2e-desktop-app
e2e-desktop-app: ## Deterministic desktop-app Playwright E2E (validates context → pf → seed → test). Override with E2E_CONTEXT=<ctx>
@if [ "$(E2E_CONTEXT)" = "$(E2E_PROD_CONTEXT)" ]; then \
echo "[E2E-GUARD] Production context $(E2E_PROD_CONTEXT) is hard-blocked." >&2; exit 1; \
fi
@echo "$(E2E_DESKTOP_ALLOWED_CONTEXTS)" | tr ' ' '\n' | grep -qx "$(E2E_CONTEXT)" || { \
echo "[E2E-GUARD] E2E_CONTEXT=$(E2E_CONTEXT) not in allow-list: $(E2E_DESKTOP_ALLOWED_CONTEXTS)" >&2; exit 1; \
}
@echo "[E2E-GUARD] Target context: $(E2E_CONTEXT)"
@kubectl config use-context "$(E2E_CONTEXT)" >/dev/null
@echo "[E2E-GUARD] Killing stale port-forwards on 8090/8091/8094..."
@lsof -ti tcp:8090 -sTCP:LISTEN 2>/dev/null | xargs -r kill 2>/dev/null || true
@lsof -ti tcp:8091 -sTCP:LISTEN 2>/dev/null | xargs -r kill 2>/dev/null || true
@lsof -ti tcp:8094 -sTCP:LISTEN 2>/dev/null | xargs -r kill 2>/dev/null || true
@sleep 1
@if [ "$(E2E_CONTEXT)" = "clerum-test" ]; then \
echo "[E2E-GUARD] Starting port-forwards against clerum-test..."; \
kubectl --context=$(E2E_CONTEXT) port-forward svc/control-api -n control-plane 8090:8090 >/tmp/pf-control-api.log 2>&1 & \
kubectl --context=$(E2E_CONTEXT) port-forward svc/external-rest-api -n profiles 8091:8091 >/tmp/pf-external-rest.log 2>&1 & \
kubectl --context=$(E2E_CONTEXT) port-forward svc/rpc-proxy -n rpc-proxy 8094:8094 >/tmp/pf-rpc-proxy.log 2>&1 & \
sleep 3; \
echo "[E2E-GUARD] Seeding test data..."; \
scripts/minikube/seed-test-data.sh; \
else \
echo "[E2E-GUARD] context=$(E2E_CONTEXT) — expecting URLs in desktop-app/.env.e2e to target GKE dev ingress. Skipping localhost pf + minikube seed."; \
fi
@echo "[E2E-GUARD] Launching Playwright..."
cd desktop-app && E2E_K8S_CONTEXT=$(E2E_CONTEXT) npm run test:e2e:playwright
# ── Local Frontends ────────────────────────────────────────────────
.PHONY: local-web
local-web: ## Run Control UI locally against minikube control-api port-forward
@set -euo pipefail; \
cleanup() { \
local pids; \
pids="$$(jobs -p || true)"; \
if [ -n "$$pids" ]; then kill $$pids 2>/dev/null || true; fi; \
wait || true; \
}; \
is_port_open() { \
local port="$$1"; \
(echo >"/dev/tcp/127.0.0.1/$$port") >/dev/null 2>&1; \
}; \
ensure_port_free() { \
local name="$$1"; \
local port="$$2"; \
if is_port_open "$$port"; then \
echo "$$name port $$port is already in use on 127.0.0.1" >&2; \
return 1; \
fi; \
}; \
wait_for_port() { \
local name="$$1"; \
local port="$$2"; \
local pid="$$3"; \
local health_url="$${4:-}"; \
local exit_code=0; \
local deadline=$$((SECONDS + $${LOCAL_UI_API_READY_TIMEOUT_SECONDS:-90})); \
while true; do \
if [ -n "$$health_url" ] && curl -fsS --max-time 2 "$$health_url" >/dev/null 2>&1; then \
echo "$$name ready at $$health_url"; \
return 0; \
fi; \
if [ -z "$$health_url" ] && is_port_open "$$port"; then \
echo "$$name ready on 127.0.0.1:$$port"; \
return 0; \
fi; \
if ! kill -0 "$$pid" 2>/dev/null; then \
wait "$$pid" || exit_code=$$?; \
echo "$$name exited before port $$port became ready" >&2; \
return 1; \
fi; \
if (( SECONDS >= deadline )); then \
if [ -n "$$health_url" ]; then \
echo "Timed out waiting for $$name at $$health_url" >&2; \
else \
echo "Timed out waiting for $$name on port $$port" >&2; \
fi; \
return 1; \
fi; \
sleep 0.25; \
done; \
}; \
trap cleanup EXIT INT TERM; \
local_context="$(LOCAL_KUBE_CONTEXT)"; \
if [ -z "$$local_context" ]; then \
local_context="$$(MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" GCP_DEV_CONTEXT="$(GCP_DEV_CONTEXT)" GCP_PROD_CONTEXT="$(GCP_PROD_CONTEXT)" scripts/dev/resolve-local-ui-kube-context.sh)"; \
fi; \
echo "Using Kubernetes context $$local_context for local-web port-forward"; \
ensure_port_free control-api 8090; \
$(MAKE) --no-print-directory MINIKUBE_PROFILE="$$local_context" minikube-pf-control-api & \
api_pid=$$!; \
wait_for_port control-api 8090 "$$api_pid" http://127.0.0.1:8090/health; \
CONTROL_API_INTERNAL_URL=http://localhost:8090 npm --prefix control-ui run dev
.PHONY: local-app-onboarding
local-app-onboarding: ## Run Desktop App showing the first-run onboarding flow (isolated; real environments untouched)
@$(MAKE) --no-print-directory EVENFIRE_ONBOARDING_PREVIEW=true local-app
.PHONY: local-app
local-app: ## Run Desktop App locally against minikube API port-forwards (EVENFIRE_ONBOARDING_PREVIEW=true previews onboarding)
@set -euo pipefail; \
cleanup() { \
local pids; \
pids="$$(jobs -p || true)"; \
if [ -n "$$pids" ]; then kill $$pids 2>/dev/null || true; fi; \
wait || true; \
}; \
is_port_open() { \
local port="$$1"; \
(echo >"/dev/tcp/127.0.0.1/$$port") >/dev/null 2>&1; \
}; \
ensure_port_free() { \
local name="$$1"; \
local port="$$2"; \
if is_port_open "$$port"; then \
echo "$$name port $$port is already in use on 127.0.0.1" >&2; \
return 1; \
fi; \
}; \
wait_for_port() { \
local name="$$1"; \
local port="$$2"; \
local pid="$$3"; \
local health_url="$${4:-}"; \
local exit_code=0; \
local deadline=$$((SECONDS + $${LOCAL_UI_API_READY_TIMEOUT_SECONDS:-90})); \
while true; do \
if [ -n "$$health_url" ] && curl -fsS --max-time 2 "$$health_url" >/dev/null 2>&1; then \
echo "$$name ready at $$health_url"; \
return 0; \
fi; \
if [ -z "$$health_url" ] && is_port_open "$$port"; then \
echo "$$name ready on 127.0.0.1:$$port"; \
return 0; \
fi; \
if ! kill -0 "$$pid" 2>/dev/null; then \
wait "$$pid" || exit_code=$$?; \
echo "$$name exited before port $$port became ready" >&2; \
return 1; \
fi; \
if (( SECONDS >= deadline )); then \
if [ -n "$$health_url" ]; then \
echo "Timed out waiting for $$name at $$health_url" >&2; \
else \
echo "Timed out waiting for $$name on port $$port" >&2; \
fi; \
return 1; \
fi; \
sleep 0.25; \
done; \
}; \
trap cleanup EXIT INT TERM; \
local_context="$(LOCAL_KUBE_CONTEXT)"; \
if [ -z "$$local_context" ]; then \
local_context="$$(MINIKUBE_PROFILE="$(MINIKUBE_PROFILE)" GCP_DEV_CONTEXT="$(GCP_DEV_CONTEXT)" GCP_PROD_CONTEXT="$(GCP_PROD_CONTEXT)" scripts/dev/resolve-local-ui-kube-context.sh)"; \
fi; \
echo "Using Kubernetes context $$local_context for local-app port-forwards"; \
ensure_port_free control-api 8090; \
ensure_port_free external-rest-api 8091; \
ensure_port_free rpc-proxy 8094; \
$(MAKE) --no-print-directory MINIKUBE_PROFILE="$$local_context" minikube-pf-control-api & \
control_api_pid=$$!; \
$(MAKE) --no-print-directory MINIKUBE_PROFILE="$$local_context" minikube-pf-external-api & \
external_api_pid=$$!; \
$(MAKE) --no-print-directory MINIKUBE_PROFILE="$$local_context" minikube-pf-rpc-proxy & \
rpc_proxy_pid=$$!; \
wait_for_port control-api 8090 "$$control_api_pid" http://127.0.0.1:8090/health; \
wait_for_port external-rest-api 8091 "$$external_api_pid" http://127.0.0.1:8091/health; \
wait_for_port rpc-proxy 8094 "$$rpc_proxy_pid" http://127.0.0.1:8094/health; \
env -u ELECTRON_RUN_AS_NODE EXTERNAL_REST_API_BASE_URL=http://127.0.0.1:8091 RPC_PROXY_BASE_URL=http://127.0.0.1:8094 CONTROL_API_BASE_URL=http://127.0.0.1:8090 EVENFIRE_ONBOARDING_PREVIEW="$(EVENFIRE_ONBOARDING_PREVIEW)" npm --prefix desktop-app run dev
.PHONY: local-ui
local-ui: ## Run Control UI, Profile UI, and Desktop App locally against minikube port-forwards
@set -euo pipefail; \
cleanup() { \
local pids; \
pids="$$(jobs -p || true)"; \
if [ -n "$$pids" ]; then kill $$pids 2>/dev/null || true; fi; \
wait || true; \
}; \
is_port_open() { \
local port="$$1"; \
(echo >"/dev/tcp/127.0.0.1/$$port") >/dev/null 2>&1; \
}; \
ensure_port_free() { \
local name="$$1"; \
local port="$$2"; \
if is_port_open "$$port"; then \
echo "$$name port $$port is already in use on 127.0.0.1" >&2; \
return 1; \
fi; \
}; \
wait_for_port() { \
local name="$$1"; \
local port="$$2"; \
local pid="$$3"; \