-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy path.env.example
More file actions
655 lines (610 loc) · 39.8 KB
/
Copy path.env.example
File metadata and controls
655 lines (610 loc) · 39.8 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
# Nexus Gateway — environment variable contract.
#
# This file is committed and documents EVERY environment variable the four
# Go services (nexus-hub / control-plane / ai-gateway / compliance-proxy)
# consume at runtime.
#
# Local dev:
# 1. Copy this file to `.env` (already gitignored).
# 2. Replace `CHANGE_ME_*` placeholders with real values.
# 3. `bootenv.LoadFromRepoRoot()` (called at service startup) auto-loads
# `.env` from repo root; no `source .env` needed.
#
# Production:
# - systemd: `EnvironmentFile=/etc/nexus-gateway/env` on each service unit.
# - Kubernetes: a Secret mounted as env, e.g. `envFrom: [secretRef: nexus-secrets]`.
# - Docker: `--env-file /etc/nexus-gateway/env`.
# - The application NEVER reads `.env` files in prod (no `.env` exists);
# env vars are injected by the deployment system before the binary starts.
#
# Precedence (godotenv non-overload):
# existing process env > .env file values
# So a one-off `MY_VAR=x ./svc` overrides this file's value for that run.
#
# Values tagged [MUST MATCH] must be identical across all four services;
# a 403 between Control Plane and Compliance Proxy is almost always these
# two values drifting out of sync.
# ─────────────────────────────────────────────────────────────────────────────
# Inter-service authentication (REQUIRED, shared across services)
# ─────────────────────────────────────────────────────────────────────────────
# [MUST MATCH all 4 services] — bearer on the Hub WebSocket registration path +
# the Hub /api/internal/things/* device-API + X-RS-Token on /v1/ai-guard/classify.
# Also the Bearer the Control Plane presents to the AI Gateway /internal/* operator
# admin endpoints (provider-test, routing-simulate, credential probe, hooks-test,
# embedding-probe, semantic-prewarm) and that the gateway requires on those routes.
# Mismatch ⇒ Hub rejects Thing registration with 401, and CP→gateway admin tools 401.
# NOTE (SEC-W2-02): this token NO LONGER gates the Hub config-WRITE surface
# (/api/hub) or admin-alerts — those moved to HUB_CONFIG_TOKEN below, so a leak of a
# data-plane service's service token can no longer inject fleet config.
INTERNAL_SERVICE_TOKEN=CHANGE_ME_INTERNAL_SERVICE_TOKEN
# [MUST MATCH control-plane <-> nexus-hub] — bearer the Control Plane presents on
# its HTTP calls to Hub's config-WRITE surface (/api/hub/*: shadow / desired-state
# push, jobs, DLQ, enrollment tokens) and the admin-alerts surface
# (/api/v1/admin/alerts/*). Control Plane is the SOLE caller of both groups; the
# data-plane services (ai-gateway, compliance-proxy) do NOT hold this token. Hub
# REQUIRES it (fails closed at boot if unset — an empty token would accept an empty
# bearer). Mismatch ⇒ every CP config push / alert admin call 403s. Separated from
# INTERNAL_SERVICE_TOKEN per SEC-W2-02 (FIX-5/C) so the config-write authority is
# not the same credential the whole fleet shares. Generate with: openssl rand -hex 32
HUB_CONFIG_TOKEN=CHANGE_ME_HUB_CONFIG_TOKEN
# [MUST MATCH across peer Hubs] — HMAC-SHA256 key that authenticates inter-Hub
# nexus.hub.signal frames (SEC-C3-01). Single-Hub deployments may leave this
# UNSET: the Hub generates a per-process random secret at boot, which still
# rejects every forged cross-Hub signal (a Hub never verifies its own skipped
# signals). Set it ONLY for multi-Hub HA, identical on every peer, so legitimate
# cross-Hub config propagation verifies. Generate with: openssl rand -hex 32
# HUB_SIGNAL_HMAC_SECRET=
# [MUST MATCH control-plane <-> ai-gateway] — HMAC-SHA256 key used to hash
# Virtual Key and Admin API key bytes before DB storage. EITHER this single
# secret OR the versioned ADMIN_KEY_HMAC_KEY_MAP below is required (set one).
# Rotating this single secret invalidates all existing keys at once (clients must
# re-issue) — use the keymap below for non-disruptive rotation instead.
ADMIN_KEY_HMAC_SECRET=CHANGE_ME_HMAC_SECRET
# Optional — versioned HMAC keyring for NON-DISRUPTIVE rotation (SEC-W2-01
# Layer A). [MUST MATCH control-plane <-> ai-gateway]. Takes precedence over
# ADMIN_KEY_HMAC_SECRET when present, and is STANDALONE: a deployment may set ONLY
# ADMIN_KEY_HMAC_KEY_MAP (leaving ADMIN_KEY_HMAC_SECRET empty) and both services
# boot. Format mirrors CREDENTIAL_KEY_MAP: "v1:<secret>,*v2:<secret>" with exactly
# one "*"-marked CURRENT version (hashes NEWLY issued keys); no marker → LAST
# wins. Every version is tried on admission, so a key issued under an older
# version keeps authenticating: admin keys are lazy-rehashed to the current
# version on their next CP auth; virtual keys ride try-all and are pruned by
# re-issue/expiry (the ai-gw admission path is read-only). Because an HMAC is
# one-way, this try-all + lazy-rehash IS the rotation mechanism — there is no
# fleet lockstep flip. Secrets MUST be comma-free (the delimiter); colons are OK
# (split on the first). Generate each with `openssl rand -hex 32`. Secrets are
# whitespace-trimmed in BOTH the single var and the map entries, so a trailing
# newline from a file-sourced value never changes the hash.
ADMIN_KEY_HMAC_KEY_MAP=
# ─────────────────────────────────────────────────────────────────────────────
# Credential encryption (REQUIRED for ai-gateway + control-plane)
# ─────────────────────────────────────────────────────────────────────────────
#
# SECRET CUSTODY (SEC-W2-03, server services): the crown-jewel root secrets below
# (CREDENTIAL_ENCRYPTION_KEY / CREDENTIAL_KEY_MAP / ADMIN_KEY_HMAC_SECRET) can be
# held WRAPPED at rest instead of plaintext. Set `secretCustody.provider:
# command` in the service yaml (control-plane / ai-gateway) with a decrypt argv
# (aws kms / sops / age / vault); then each of these env vars carries a base64
# WRAPPED blob that the service unwraps once at boot via its own KMS grant.
# Default `provider: noop` (or omitted) reads them RAW as below — dev / simplest
# appliance mode. The [MUST MATCH] contract is on the UNWRAPPED plaintext (each
# service may wrap under its own grant). Desktop agents do NOT hold these and use
# an OS keystore instead (SEC-M4-02), not this mechanism.
# `provider: command` is wired end-to-end across all four server services
# (control-plane / ai-gateway / nexus-hub each unwrap their own crown jewels at
# boot; the compliance-proxy CA key uses the same mechanism via ca.kms). To wrap
# in production, see the SECRET CUSTODY section of the prod-deploy skill
# (.claude/skills/prod-deploy/SKILL.md): generate each wrapped blob out-of-band,
# grant each service kms:Decrypt on only its own secrets, and set
# secretCustody.provider/command in that service's yaml.
# [MUST MATCH control-plane <-> ai-gateway <-> nexus-hub] — AES-256 master key,
# 64 hex chars (32 bytes). Encrypts provider API keys at rest (control-plane /
# ai-gateway) and alert-channel config secrets at rest (nexus-hub: SMTP
# password, Slack bot token, PagerDuty routing key, sensitive webhook headers).
# REQUIRED for nexus-hub: it fails closed at boot if this is unset or malformed
# rather than storing alert secrets as cleartext.
# Generate via:
# openssl rand -hex 32
CREDENTIAL_ENCRYPTION_KEY=CHANGE_ME_ENCRYPTION_KEY_64HEX_CHARS
# Optional — multi-key map for credential-key rotation. Takes precedence over
# CREDENTIAL_ENCRYPTION_KEY when present, and is STANDALONE: a deployment may
# set ONLY CREDENTIAL_KEY_MAP (leaving CREDENTIAL_ENCRYPTION_KEY empty) and both
# the AI Gateway and the Control Plane boot — the map provides all
# encrypt/decrypt operations. Format: "v1:<hex64>,v2:<hex64>".
# The key used for NEW encryptions is chosen by prefixing exactly one entry's
# id with "*" (e.g. "v1:<hex64>,*v2:<hex64>" → v2 is current). With no "*"
# marker the LAST entry wins. Mark the current key explicitly so prepending a
# rotation key never silently changes which key encrypts.
CREDENTIAL_KEY_MAP=
# ─────────────────────────────────────────────────────────────────────────────
# Compliance Proxy runtime API (REQUIRED for CP + compliance-proxy)
# ─────────────────────────────────────────────────────────────────────────────
# [MUST MATCH control-plane <-> compliance-proxy] — bearer for compliance-proxy
# /runtime/* endpoints (read GETs + the break-glass PUT). CP's admin UI
# passthrough sends it; compliance-proxy validates it. Mismatch ⇒ 401 on admin
# proxy ops.
COMPLIANCE_PROXY_API_TOKEN=CHANGE_ME_COMPLIANCE_PROXY_API_TOKEN
# ─────────────────────────────────────────────────────────────────────────────
# AI Gateway runtime API (ai-gateway only)
# ─────────────────────────────────────────────────────────────────────────────
#
# The ai-gateway /runtime/* admin endpoints (and /debug/runtime, /metrics, and
# the /internal/* operator routes) are gated on INTERNAL_SERVICE_TOKEN — the
# same [MUST MATCH] secret the gateway already uses for the Hub WS/HTTP
# transport. There is no separate ai-gateway runtime token (the previous
# AI_GATEWAY_API_TOKEN was an unvalidated 4th token; removed in F-0243).
# Performance flags — all OPTIONAL and non-secret. Every one ships with its
# optimal value as the CODE default, so a stock deployment needs none of them;
# set a variable only to diverge from the default.
# NEXUS_LAZY_CANONICAL — compute the request canonical only when a synchronous
# consumer needs it (smart routing / response cache); otherwise leave it nil
# and let the async audit writer derive it off the latency path. On the clean
# path (hooks-off, simple routing, cache-off) this skips the eager request-body
# Normalize entirely (~29% of request-path CPU on a 50 KB body). DEFAULT ON;
# NEXUS_LAZY_CANONICAL=0 forces always-compute. See normalization-architecture.md §5.2.
# NEXUS_CGO_SCAN_LIMIT — cap concurrent hook content-scan cgo crossings.
# DEFAULT "auto" (≈ CPUs − 2): tames the M-oversubscription tail under high
# hooks-on concurrency. "0" disables the cap; a positive integer pins it.
# AI_GATEWAY_AUDIT_CODEC — inline-body compression codec on the audit side-path.
# DEFAULT "s2" (faster; the larger frame is covered by the spool quota).
# "zstd" trades CPU for a smaller frame.
# NEXUS_AUDIT_WIRE — gw→hub audit wire. DEFAULT "binary" (the Hub dual-reads).
# "json" reverts to the legacy text wire.
# AI_GATEWAY_AUDIT_LOSS_MODE — audit overflow policy. DEFAULT "spillblock"
# (no-loss: the durable on-disk spool is the primary overflow buffer, and the
# request path back-pressures only when that spool is ALSO saturated).
# "block" = back-pressure at the in-heap queue, bounded at 10s, then a durable
# spool write (a counted drop only if no spool is wired). LOSSY opt-outs, for callers that do not need a compliance
# audit trail: "spill" (async durable spill, counted drop once it saturates)
# and "drop" (counted bounded drop). An empty or unrecognised value resolves
# to "spillblock", so a typo can never make the trail lossy.
# NEXUS_QUOTA_WRITE_BEHIND / NEXUS_CREDSTATS_WRITE_BEHIND — defer quota and
# credential-stats Redis writes off the request hot path (flush on an interval,
# final drain on graceful shutdown). DEFAULT ON (soft quota). Overshoot per
# instance ≤ read-cache TTL + flush interval (~1.25s); across an N-instance
# fleet the blind-spend window is that × N (each instance is unaware of peers'
# un-flushed spend), and a hard kill loses the un-flushed increments. Set to 0
# for strict synchronous per-request accounting.
# NEXUS_EVENTS_MAX_BYTES — NEXUS_EVENTS audit-stream cap. DEFAULT "auto"
# (15% of total RAM; logs a WARN at startup with the chosen value). Pin a fixed
# size to override, e.g. NEXUS_EVENTS_MAX_BYTES=32GB. Alias: NEXUS_STREAM_MAX_BYTES.
# NEXUS_EVENTS_STORAGE — NEXUS_EVENTS storage tier. DEFAULT "memory" (the audit
# stream is a delay-tolerant burst buffer; keeping it in RAM frees the data disk
# for the durable Postgres writes, the single largest single-box throughput
# lever). Trade-off: a NATS broker restart/crash drops published-but-undrained
# events (those already reclaimed from the producer spill); the overflow→disk
# no-loss path only covers the stream-full case, not a broker bounce. Set
# NEXUS_EVENTS_STORAGE=file for a durable file-backed stream that survives a
# broker restart at the cost of the steady-state disk writes.
# GOMEMLIMIT — Go runtime soft memory limit (read by the Go runtime, not our
# code). When UNSET, each service auto-sets it at boot from the cgroup memory
# limit (~70% of the cgroup max) when one is present, and logs a WARN with the
# chosen value and how to override; if no cgroup limit is detectable it is left
# unset (no soft cap). Without a soft cap a burst of large request/response
# bodies can grow the heap until the kernel OOM-kills the service (observed under
# high-concurrency SSE). To pin it explicitly, set ~70% of the box/cgroup memory,
# e.g. GOMEMLIMIT=22GiB on a 32 GiB box. The AMI/systemd deployment also stamps
# it; the auto-set covers hand-rolled and container deployments that don't.
# NEXUS_PPROF_ADDR=:6060 — bind a net/http/pprof server for profiling. Unset
# in production unless actively profiling.
# ─────────────────────────────────────────────────────────────────────────────
# Infrastructure URLs (REQUIRED; vary per environment)
# ─────────────────────────────────────────────────────────────────────────────
# Postgres — all four services. Local dev runs Postgres in Docker on 55532.
# NOTE: DATABASE_URL carries only the connection target (host/port/db/auth).
# Per-service connection-POOL sizing now lives in each service's yaml under
# `database.maxConns` / `database.minConns` (NOT in this connection string).
# Those pools are already coordinated to stay under Postgres
# `max_connections=200`: hub 40 + ai-gateway 25 + control-plane 15 +
# compliance-proxy 4 (config-only) = 84. To raise concurrency, edit the
# service yaml — not this URL.
DATABASE_URL=postgresql://postgres:postgres@localhost:55532/nexus_gateway?sslmode=disable
# Redis — all four services consume the same universal contract via
# packages/shared/storage/redisfactory. Local dev runs on 6437; prod on
# 6379. Every variable below corresponds to a yaml field of the same name
# under the service's `redis:` block; env values win over yaml (L3 > L2).
# See docs/developers/architecture/cross-cutting/foundation/configuration-architecture.md §9 for the
# field-by-field contract.
#
# Mode + addrs (REQUIRED to enable Redis; empty addrs disables it).
REDIS_MODE=standalone
REDIS_ADDRS=localhost:6437
#
# Auth — REDIS_USERNAME blank uses the legacy AUTH-only flow.
# REDIS_USERNAME=
# REDIS_PASSWORD=
# REDIS_DB=0
#
# Sentinel sub-block (only used when REDIS_MODE=sentinel).
# REDIS_SENTINEL_MASTER_NAME=mymaster
# REDIS_SENTINEL_USERNAME=
# REDIS_SENTINEL_PASSWORD=
#
# Cluster sub-block (only used when REDIS_MODE=cluster).
# REDIS_CLUSTER_MAX_REDIRECTS=8
# REDIS_CLUSTER_ROUTE_RANDOMLY=false
# REDIS_CLUSTER_READ_ONLY=false
#
# TLS / mTLS sub-block.
# REDIS_TLS_ENABLED=false
# REDIS_TLS_INSECURE=false
# REDIS_TLS_CA_FILE=/etc/nexus/redis/ca.pem
# REDIS_TLS_CERT_FILE=/etc/nexus/redis/client.crt
# REDIS_TLS_KEY_FILE=/etc/nexus/redis/client.key
# REDIS_TLS_SERVER_NAME=redis.example.com
#
# Pool tuning.
# IMPORTANT: the REAL high-concurrency default is set in each service's yaml
# `redis:` block (poolSize=200 / minIdleConns=50), already tuned for
# production load. The two env vars below are OVERRIDE-ONLY escape hatches
# for special cases — they do NOT represent the shipped default. The sample
# values here (10 / 0) are the upstream go-redis library defaults, which are
# LOWER than the yaml: uncommenting `REDIS_POOL_SIZE=10` would DOWNGRADE
# concurrency from 200 to 10. Leave both commented unless you have a
# specific reason to diverge from the yaml.
# REDIS_POOL_SIZE=10
# REDIS_MIN_IDLE_CONNS=0
# REDIS_MAX_RETRIES=3
#
# Timeouts (Go time.ParseDuration syntax: "5s", "200ms", "1m30s").
# REDIS_DIAL_TIMEOUT=5s
# REDIS_READ_TIMEOUT=3s
# REDIS_WRITE_TIMEOUT=3s
# REDIS_POOL_TIMEOUT=4s
# NATS JetStream — message queue for cross-service events.
# NATS_URL is the only per-service NATS knob. The NATS SERVER-side limits
# (max_payload=16MB, max_connections, etc.) are configured where the NATS
# server itself runs: the appliance (nexus-ami) and docker-compose.yml — not
# here. This URL only tells each service where to find that server.
NATS_URL=nats://localhost:4222
# AI Gateway audit spill (optional override). When the in-memory traffic_event
# buffer is full after backpressure, overflow records spill to NDJSON on disk
# instead of being dropped. The yaml default spoolDir (/var/lib/nexus/audit-
# spool) is writable on the appliance; for a local `go run` dev box that path
# usually does not exist, so the spill auto-disables with a warning. Point this
# at a writable local dir to enable spill in dev. Empty/unset keeps the yaml
# default.
# AI_GATEWAY_AUDIT_SPOOL_DIR=/var/lib/nexus/audit-spool
# In-heap audit record-buffer cap (overflow → durable spill above). Bounds the
# queue's POINTER count only — the memory bound is AI_GATEWAY_AUDIT_MEM_MAX_BYTES
# below, which accounts each record's REAL body bytes. 0/unset keeps the default.
# AI_GATEWAY_AUDIT_MAX_QUEUED_RECORDS=10000
# In-memory audit byte budget — the memory half of the bounded audit queue. The
# audit writer reserves each record's REAL captured body bytes against this budget
# at admission; on a full budget the no-loss modes back-pressure the request path
# (RPS throttles to the audit drain rate, nothing dropped) and the lossy modes
# shed with a counted drop. Semantics mirror NEXUS_EVENTS_MAX_BYTES: unset/"auto"
# auto-sizes to ~15% of available RAM (2 GiB fallback off-Linux); an explicit
# human size ("8GB", "2048MB", raw bytes) pins it. A bigger budget buys BURST
# absorption (audit work deferred to quieter moments), not sustained throughput —
# under sustained overload the drain rate is the ceiling regardless. SIZING: the
# budget accounts raw body bytes only; REAL process memory runs ~2x the budget
# (Go GC headroom + marshal/compression copies), and the NATS memory stream holds
# another ~15% of RAM on the same box — keep a pin <=20% of RAM (a 10GB pin on a
# 32GB box OOM-killed the gateway at 15.9GB RSS in rig validation; a pin >25%
# logs a WARN). Observability: nexus_audit_mem_backpressure_total counts
# budget-full events; the resolved value is logged at startup.
# AI_GATEWAY_AUDIT_MEM_MAX_BYTES=auto
# In-flight request admission cap. Bounds concurrent proxy requests so overload
# degrades into fast retryable 429s (Retry-After: 1, OpenAI-shaped error body)
# instead of unbounded in-heap queueing toward the GOMEMLIMIT collapse. Health,
# metrics, and admin endpoints are never gated. Shed requests are counted on
# nexus_ai_gateway_admission_shed_total (no audit record — auditing a shed storm
# would itself be load). DEFAULT unset/"auto" = 1024 x GOMAXPROCS — scaled by
# core count and sized generously so SSE streams (which hold a slot for the
# stream's full duration) do not false-shed a healthy streaming deployment; the
# bound caps heap growth under overload, not normal traffic. 0/negative
# disables; a non-numeric value falls back to auto with a warning.
# AI_GATEWAY_MAX_INFLIGHT=auto
# Built-in per-VK caps for expensive generative endpoints (e88 NFR-4). These
# close the billing-DoS surface where one leaked/abusive virtual key opens
# unbounded concurrent per-call-priced requests — the global admission gate
# above is process-wide, not per-VK. Defaults are code constants; override only
# to tune. CONCURRENCY = max simultaneous in-flight requests of that kind PER
# VK (0 = unlimited — disabling a DoS control logs a WARN). MAX_BYTES = a
# per-kind request-body ceiling, tighter than payload_capture.maxRequestBytes.
# NOTE: raising CONCURRENCY is ALL-VK on this single-tenant box, not per-key.
# AI_GATEWAY_GENERATIVE_CAP_IMAGE_GENERATION_CONCURRENCY=4
# AI_GATEWAY_GENERATIVE_CAP_IMAGE_GENERATION_MAX_BYTES=262144
# AI_GATEWAY_GENERATIVE_CAP_TTS_CONCURRENCY=8
# AI_GATEWAY_GENERATIVE_CAP_TTS_MAX_BYTES=262144
# AI_GATEWAY_GENERATIVE_CAP_VIDEO_GENERATION_CONCURRENCY=2
# AI_GATEWAY_GENERATIVE_CAP_VIDEO_GENERATION_MAX_BYTES=16777216
# Video render bound: max NON-TERMINAL jobs (queued/in_progress) per VK — the
# real bound on concurrent paid renders (VIDEO_GENERATION_CONCURRENCY above
# bounds only in-flight HTTP requests). 429 VIDEO_JOBS_LIMIT at the cap.
# AI_GATEWAY_VIDEO_MAX_NONTERMINAL_JOBS=4
# AI_GATEWAY_GENERATIVE_CAP_STT_CONCURRENCY=4
# AI_GATEWAY_GENERATIVE_CAP_STT_MAX_BYTES=27262976
# AI_GATEWAY_GENERATIVE_CAP_GUARDRAIL_CONCURRENCY=4
# AI_GATEWAY_GENERATIVE_CAP_GUARDRAIL_MAX_BYTES=1048576
# Realtime counts long-lived WebSocket SESSIONS per VK (acquired at upgrade,
# released at session close), and its MAX_BYTES is the per-WS-FRAME ceiling
# enforced via SetReadLimit on both legs — not an HTTP body cap. One
# server-to-server VK serves a whole product: the default 2 is a dark-launch
# floor; production realtime use requires raising CONCURRENCY (all-VK).
# AI_GATEWAY_GENERATIVE_CAP_REALTIME_CONCURRENCY=2
# AI_GATEWAY_GENERATIVE_CAP_REALTIME_MAX_BYTES=16777216
# Audit overflow policy (shared/audit/lossmode; the same four modes the
# compliance proxy and the agent use). Durable audit is a product promise +
# compliance requirement, so the DEFAULT "spillblock" is NO-LOSS: on a full
# in-heap buffer, overflow goes to the durable on-disk spool off the request
# path, and the request path back-pressures ONLY when that large spool is also
# saturated — including on a full spool quota, where the spill worker keeps the
# batch and retries while the recovery sweeper frees space, so ingest
# self-throttles to the drain rate instead of shedding records.
#
# Alternatives: "block" = back-pressure at the in-heap queue, which stalls sooner
# and is bounded at 10s; past that bound it falls back to a durable spool write,
# and counts a drop only if no spool is wired. LOSSY opt-outs, for callers that do not
# need a compliance audit trail: "spill" = async durable spill with a counted
# drop once that path is also saturated; "drop" = counted bounded drop, maximum
# throughput. Both count on dropped_total — never silently.
#
# Empty or unrecognised → "spillblock". The values are matched exactly and in
# lower case, and anything else resolves to the no-loss default rather than
# being rejected, so a typo can never make the audit trail lossy.
# AI_GATEWAY_AUDIT_LOSS_MODE=spillblock
# End-to-end zstd compression of large captured audit bodies. The producer
# compresses off the request path (async marshal worker), the body rides the
# NATS wire compressed, the Hub persists the compressed bytes verbatim (no
# decompress on ingest), and only the Control-Plane view layer decompresses.
# Captured bodies are JSON/text (~3-10x), and the audit pipeline is disk-I/O-
# bound at the NATS broker, so this is the direct lever on publish throughput.
# Default true; set 0/false to disable.
# AI_GATEWAY_AUDIT_COMPRESS=true
# Smallest captured body worth compressing (zstd frame + base64 overhead can
# exceed savings below this). 0/unset → 1024.
# AI_GATEWAY_AUDIT_COMPRESS_MIN_BYTES=1024
# zstd encoder level (1=fastest, 3=default, higher=better ratio/slower).
# 0/unset → library default.
# AI_GATEWAY_AUDIT_COMPRESS_LEVEL=3
# Spill-recovery sweeper: replays sealed on-disk spool files back into the MQ
# queue so a record that overflowed to disk still reaches the queryable store
# (the drain half of spill-defer). ON by default whenever a spool dir is set —
# a durable spool that never reaches Postgres is a silent data gap. Interval =
# sweep period; pace = throttle between files (yields the box to the request
# path). 0/unset → 2000 ms / 50 ms. Set INTERVAL_MS negative to DISABLE.
# AI_GATEWAY_AUDIT_SPILL_RECOVERY_INTERVAL_MS=2000
# AI_GATEWAY_AUDIT_SPILL_RECOVERY_PACE_MS=50
# ---------------------------------------------------------------------------
# BENCHMARK / ABLATION SWITCHES — NOT FOR PRODUCTION
# These exist only to measure throughput against forward-only gateways.
# Do NOT copy these into any production or customer environment file.
# ---------------------------------------------------------------------------
# NEXUS_PERF_PURE_FORWARD — BENCHMARK ONLY. Set to 1 to make the AI Gateway a
# pure forwarding proxy: it SKIPS the entire audit tail, so NO traffic_event
# rows are written. This DESTROYS the audit trail (GDPR / audit-retention
# loss) and must NEVER be set in any production or customer environment.
# Default (unset) = audit stored, normal operation. Detection when active:
# the startup WARN banner "PURE-FORWARD BENCHMARK MODE ACTIVE" and the
# Prometheus gauge nexus_ai_gateway_pure_forward_mode == 1.
# Intentionally has NO assignable line here so it is never accidentally shipped.
# Service discovery — co-located services on localhost; on prod each is a
# domain or LB. Every URL below is bare-named (no service prefix) because
# it identifies a shared environment-level entity, not a service-private
# knob. See docs/developers/architecture/cross-cutting/foundation/configuration-architecture.md §6.
#
# Hub registration URL. ai-gateway, control-plane, and compliance-proxy all
# read this to register as Things on the Hub via thingclient.
NEXUS_HUB_URL=http://127.0.0.1:3060
# Peer service URLs are NOT configured — a service never configures another
# Nexus service's address. Each service reports its own publicURL + privateURL
# to the Hub at registration, and peers resolve the Hub-reported value at
# request time (shared/transport/peerurl). Each service's own *_PRIVATE_URL
# (optional, below) overrides its auto-detected private IPv4 — set only for
# split-horizon or non-default topologies.
# NEXUS_HUB_PRIVATE_URL=http://10.0.0.10:3060
# CONTROL_PLANE_PRIVATE_URL=http://10.0.0.11:3001
# AI_GATEWAY_PRIVATE_URL=http://10.0.0.12:3050
# COMPLIANCE_PROXY_PRIVATE_URL=http://10.0.0.13:3040
# Shared OAuth/OIDC auth server. CP runs the issuer side; Hub runs the
# verifier side. Both read AUTH_SERVER_ISSUER and verify it matches —
# this is [MUST MATCH CP <-> Hub].
#
# ISSUER and JWKS_URL are not the same kind of thing and are not always the
# same value. The issuer is an IDENTITY — the `iss` claim tokens carry, which
# must be the origin a BROWSER uses. JWKS_URL is an ADDRESS something dials to
# fetch signing keys, and the CP verifies its own tokens too, so it reads this
# as well as the Hub. Where the console is published somewhere other than
# where the CP listens (a reverse proxy, or the compose quickstart, which
# publishes the console on the host while the CP runs in a container), the
# issuer origin does not resolve to the CP from inside — leave JWKS_URL
# derived from it and the CP cannot fetch its own keys, so every admin API
# call answers 401 while sign-in itself succeeds and the console bounces back
# to the login form.
AUTH_SERVER_URL=http://127.0.0.1:3001
AUTH_SERVER_JWKS_URL=http://127.0.0.1:3001/.well-known/jwks.json
AUTH_SERVER_ISSUER=http://127.0.0.1:3001
# Shared agent-CA directory. Hub mints agent certs here; CP reads the
# same directory for admin-UI introspection. Both bare-named because
# "agent CA" is a shared PKI entity, not a service-private knob.
# AGENT_CA_DIR=/var/lib/nexus/agentca
# AGENT_CA_CERT_FILE=/etc/nexus/agentca/ca.crt
# AGENT_CA_KEY_FILE=/etc/nexus/agentca/ca.key
# ─────────────────────────────────────────────────────────────────────────────
# Service-private operational knobs (OPTIONAL; sensible defaults baked into
# yaml/code). Each variable is prefixed with the owning service name —
# `<SVC>_<KNOB>` — because the value describes the service's own private
# state (its listen port, its instance id, its private feature flag).
# ─────────────────────────────────────────────────────────────────────────────
# <SVC>_HOST — bind interface for the HTTP server. Empty (default) binds all
# interfaces (":port") — what containers / Kubernetes / direct SDK access need.
# Set to 127.0.0.1 to bind loopback-only when a same-host reverse proxy (nginx)
# fronts the service and the port must not be externally reachable (the
# single-host appliance does this; it keeps ai-gateway's unauthenticated
# /internal/* debug surface off every interface but loopback).
# Hub knobs.
# NEXUS_HUB_PORT=3060
# NEXUS_HUB_HOST=127.0.0.1
# NEXUS_HUB_ID=hub-prod-1
# NEXUS_HUB_ADVERTISE_ADDR=https://hub.example.com
# NEXUS_HUB_SCHEDULER_ENABLED=true
# NEXUS_HUB_ALLOWED_ORIGINS=https://nexus.example.com
# Accept localhost WebSocket origins (dev only). MUST stay false/unset in prod.
# NEXUS_HUB_DEV_MODE=true
# NEXUS_EVENTS JetStream stream cap (audit side-path burst buffer). Accepts
# "8GB" / "512MB" / a bare byte count, or "auto"/unset. DEFAULT "auto" = 15% of
# total RAM (a WARN at startup logs the chosen value); pin a fixed size to override.
# The producer publishes full-speed and the Hub drains lazily, so this absorbs a
# long burst. The stream uses DiscardNew: at the cap, NEW audit publishes fail and
# the gateway spills them durably to disk — it does NOT discard old un-acked rows.
# Alias NEXUS_STREAM_MAX_BYTES (perf-rig name) is honoured when this is unset.
# NEXUS_EVENTS_MAX_BYTES=auto
#
# NEXUS_EVENTS storage tier. DEFAULT "memory" (in-RAM stream) — keeps the
# delay-tolerant burst buffer off the data disk so the disk serves the durable
# Postgres writes, the single largest single-box throughput lever. NOTE: the cap
# above is committed to RAM, so on a 256 GiB box "auto" (15%) commits ~38 GiB to
# the stream — size GOMEMLIMIT/cgroup accordingly. Trade-off: a NATS broker
# restart/crash drops published-but-undrained events; the overflow→disk no-loss
# path covers only the stream-full case, not a broker bounce. Set "file" for a
# durable file-backed stream that survives a restart at the cost of steady-state
# disk writes.
# NEXUS_EVENTS_STORAGE=memory
# Traffic-event drain duty cycle — how the audit drain yields CPU to a co-located
# AI-gateway core path (yaml: consumers.trafficDrainDutyCycle, this env overrides).
# Default 0.3 = FIXED throttle: reliably yields the single-box's memory bandwidth /
# loopback / Postgres to the gateway core path (measured: gateway 200-VU non-SSE
# RPS ~5150 -> ~6300, beating Bifrost 5284, no loss). 0 = ADAPTIVE CPU-pressure
# probe (best on a small/CPU-bound box; cannot see memory-bandwidth contention on
# a core-rich box). >=1 = OFF (dedicated Hub box). NATS file store absorbs the
# backlog while idle; audit is delay-tolerant, no-loss preserved by retention.
# NEXUS_HUB_AUDIT_DRAIN_DUTY_CYCLE=0.3
# Control Plane knobs.
# CONTROL_PLANE_PORT=3001
# CONTROL_PLANE_HOST=127.0.0.1
# CONTROL_PLANE_CRYPTO_PRODUCTION=true
# AUTH_SERVER_KEYSTORE_DIR=/var/lib/nexus/cp-keystore # CP-private; not shared
# Revocation endpoints the admin token checker calls. Both live on THIS auth
# server, so like AUTH_SERVER_JWKS_URL they are addresses to dial rather than
# the issuer identity; both default to the issuer + their path. Set them
# wherever that default is not reachable from inside the CP — failure here is
# quiet, logging a warning and silently no longer reconciling revoked tokens.
# AUTH_SERVER_REVOCATION_INTROSPECT_URL=http://127.0.0.1:3001/oauth/introspect
# AUTH_SERVER_REVOCATION_REPLAY_URL=http://127.0.0.1:3001/api/internal/revocations
# NODE_ENV=production
# APP_VERSION=1.0.0
# Web assistant ("Chat with Nexus") — Control Plane only.
# [SECRET] Backend system Virtual Key used ONLY for the assistant's LLM
# inference. Never sent to the browser. Absent ⇒ POST /assistant/chat returns
# 503 "assistant inference is not configured". All users' assistant inference
# runs through this one VK; its cost is a platform cost on this VK, not
# attributed per-user.
NEXUS_ASSISTANT_SYSTEM_VK=
# Default inference model (optional; code default claude-sonnet-4-6). If the
# configured default is not routable by the system VK, the best available
# routable chat model is used instead.
# NEXUS_ASSISTANT_MODEL=claude-sonnet-4-6
# Optional comma-separated allow-list of client-selectable models. EMPTY =
# auto-derive every chat model the system VK can route (no list to maintain);
# set it only to pin a narrower allow-list.
# NEXUS_ASSISTANT_MODELS=
# Production posture — set to 1 on prod deployments: tells the agent's system
# prompt the truth about the environment AND arms the backend-enforced second
# confirmation on write actions.
# NEXUS_ASSISTANT_PROD=
# Optional — set to 1 to withhold the raw-body read tools (the assistant cannot
# reach raw traffic bodies); data-governance posture.
# NEXUS_ASSISTANT_DISABLE_BODY_READS=
# Optional — wall-clock backstop on a single chat turn (Go duration; default
# 10m). Keep it below the ingress idle/read timeout.
# NEXUS_ASSISTANT_TURN_DEADLINE=10m
# AI Gateway knobs.
# AI_GATEWAY_PORT=3050
# AI_GATEWAY_HOST=127.0.0.1
# Pre-grow KiB for the request-body read scratch (server.requestReadBufKb).
# 64 default; raise to ~128 for fleets that routinely carry ~128K-token contexts.
# AI_GATEWAY_REQUEST_READ_BUF_KB=64
# AI_GATEWAY_CACHE_ENABLED=true
# AI_GATEWAY_CACHE_TTL=5m
# AI_GATEWAY_CACHE_PREFIX=ai-gw:
# AI_GATEWAY_CORS_ENABLED=true
# AI_GATEWAY_CORS_ALLOWED_ORIGINS=https://nexus.example.com
# Compliance Proxy knobs.
# (Ports live in yaml only: listener.address :3128 + runtimeApi :3040 — the
# proxy has two ports, so no single *_PORT env override exists for it.)
#
# CA / KMS signing mode is configured in the proxy yaml `ca.kms` block (argv
# only — no secrets in yaml). There are NO Nexus env vars for the KMS commands:
# the KMS credentials they use (e.g. AWS_ACCESS_KEY_ID / AWS_REGION or an
# instance role) are ambient to the proxy process and read by the cloud SDK the
# `command` / `encryptCommand` / `signCommand` invoke. In remote signing mode
# the cert-cache key is a self-bootstrapped, KMS-wrapped DEK stored in Redis
# (rotation = delete the `nexus:proxy:cert-cache-dek` key); grant the proxy's
# KMS identity kms:Encrypt + kms:Decrypt + kms:Sign. See
# docs/developers/architecture/services/compliance-proxy/compliance-proxy-tls-cert-architecture.md.
# ─────────────────────────────────────────────────────────────────────────────
# Horizontal cutters (cross-service knobs read by every binary)
# ─────────────────────────────────────────────────────────────────────────────
# Logging — override yaml log.level / log.format / log.file per env.
# LOG_LEVEL=debug
# LOG_FORMAT=json
# LOG_FILE=/var/log/nexus/<service>.log
# LOG_STACK_ON_ERROR=true
# Public URL overrides — each service's externally-reachable base URL
# (yaml `publicURL` is the primary layer; these env vars override it).
# NEXUS_HUB_PUBLIC_URL=https://hub.example.com
# CONTROL_PLANE_PUBLIC_URL=https://cp.example.com
# AI_GATEWAY_PUBLIC_URL=https://api.example.com
# COMPLIANCE_PROXY_PUBLIC_URL=https://proxy.example.com:3128
# OTEL.
# OTEL_ENDPOINT=http://otel-collector:4318
# OTEL_SERVICE_NAME=nexus-ai-gateway
# Driver selection.
# MQ_DRIVER=nats
# Control Plane UI build-time variables (Vite — must be set at `npm run build`
# time, NOT runtime; baked into the static bundle. Full types in
# packages/control-plane-ui/src/vite-env.d.ts).
#
# Absolute base URL of the AI Gateway used to build the AI-Guard compliance
# webhook URL admins copy into an external scanner. Set when the gateway is not
# co-located on the Control Plane host:3050 (separate hostname, TLS ingress,
# non-standard port). When unset, the base is derived from the browser host on
# :3050.
# VITE_AI_GATEWAY_WEBHOOK_URL=https://api.example.com
# Optional HTTPS endpoint that accepts POST JSON client error reports; unset = no reports.
# VITE_CLIENT_ERROR_REPORTING_URL=https://errors.example.com/ingest
# Optional release / build id included in client error reports (e.g. git SHA).
# VITE_APP_RELEASE=
# ── Vendor bill reconciliation (nexus-hub) ───────────────────────────────────
# Admin/org keys the daily reconciliation job uses to read each provider's
# AUTHORITATIVE billed USD from the vendor's cost API (distinct from the
# inference credentials stored in the DB). Secret → env-only, never yaml.
# A provider whose key is unset is simply skipped (not reconciled).
# Full setup + troubleshooting: docs/operators/ops/runbooks/vendor-bill-reconciliation.md
#
# The key TYPE matters — both endpoints are organization-management APIs and
# reject ordinary inference keys:
# OpenAI must be an ORGANIZATION Admin key (platform.openai.com →
# Settings → Organization → Admin keys, requires Owner role).
# A project key or service-account key returns
# 403 "Missing scopes: api.usage.read" no matter what permissions
# are granted to it.
# Anthropic must be an Admin key (sk-ant-admin01-…) created in the Console
# under Admin keys. A normal sk-ant-api03-… inference key returns
# 401 "invalid x-api-key".
#
# OpenAI org admin key (Bearer) for GET /v1/organization/costs.
# OPENAI_COST_ADMIN_KEY=
# Anthropic admin key (sk-ant-admin01-...) for GET /v1/organizations/cost_report.
# ANTHROPIC_COST_ADMIN_KEY=
#
# ── Scope pins (optional, but reconciliation is near-useless without them) ────
# An admin key reports the WHOLE organization. If the gateway is one consumer
# among several (other API keys, other products, other teams), the vendor total
# is not comparable to the gateway's spend: the row lands as coverage=org_only,
# shows ~100% difference, and never alerts. Pinning narrows the vendor number to
# just this gateway.
#
# OpenAI: narrows via the endpoint's api_key_ids filter — exact per-key
# attribution. Use the id (key_...), NOT the secret, of the key the gateway
# authenticates with. Find it in the OpenAI console, or by grouping costs by
# api_key_id and matching the daily curve against the gateway's own estimate.
# OPENAI_COST_API_KEY_ID=
#
# Anthropic: cost_report exposes NO per-key cost and NO filter parameters, so
# workspace is the finest unit available and filtering happens client-side.
# Requires a NAMED workspace containing the gateway's key — the default
# workspace is reported with a null workspace_id and can never be scoped.
# ANTHROPIC_COST_WORKSPACE_ID=