Repository navigation
Expand file tree
/
Copy pathexample.env
More file actions
1432 lines (1318 loc) · 75.8 KB
/
Copy pathexample.env
File metadata and controls
1432 lines (1318 loc) · 75.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
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
# ===========================================
# Logging Configuration
# ===========================================
# Log level for the application (default: INFO)
# Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
# XAGENT_LOG_LEVEL="INFO"
# ===========================================
# OpenTelemetry Runtime Metrics (optional)
# ===========================================
# Export runtime performance metrics over OTLP/HTTP. Export is enabled
# automatically when an endpoint is configured; this flag is an explicit
# override/kill switch.
# XAGENT_RUNTIME_TELEMETRY_ENABLED="true"
# Full OTLP/HTTP metrics endpoint, including /v1/metrics.
# XAGENT_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="http://otel-collector:4318/v1/metrics"
# Periodic export interval in milliseconds (default: 10000).
# XAGENT_OTEL_EXPORT_INTERVAL_MILLISECONDS="10000"
# OpenTelemetry service.name resource attribute (default: xagent).
# XAGENT_OTEL_SERVICE_NAME="xagent"
# Standard OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
# OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_METRIC_EXPORT_INTERVAL, and
# OTEL_SERVICE_NAME variables are also supported as fallbacks.
# Maximum total decoded Base64 attachment bytes per execution (default: 8 MiB).
# Only explicit file deliveries are converted. Zero rejects attachments with
# an unavailable notice; it does not expose the raw Base64 in the final answer.
# XAGENT_INLINE_FILE_DELIVERY_MAX_BYTES="8388608"
# ===========================================
# Docker Compose Configuration
# ===========================================
# When using docker-compose.yml, DATABASE_URL is automatically set.
# You only need to configure the PostgreSQL password below.
# POSTGRES_PASSWORD is used by both postgres and xagent services.
POSTGRES_PASSWORD="xagent_password"
# PostgreSQL image tag for the bundled postgres service (default: 17-bookworm).
# A postgres_data volume initialized by v16 must pin "16-bookworm" until it is
# migrated. See the upgrade runbook in docker/README.md.
# POSTGRES_IMAGE_TAG="16-bookworm"
# Web service port (default: 80)
# Frontend and backend are both accessible through this single port
# nginx will route /api/* to backend and others to frontend
PORT="80"
# ===========================================
# Database Configuration (optional)
# ===========================================
# For local development without Docker Compose, uncomment and configure:
# DATABASE_URL="postgresql://xagent:xagent_password@localhost:5432/xagent"
# Or use SQLite (default, no configuration needed):
# DATABASE_URL="sqlite:///home/xagent/.xagent/xagent.db"
# SQLAlchemy connection pool settings (non-SQLite databases only). These
# apply to EACH of the two engines a process may create — the shared web
# engine and the ad-hoc engine behind create_db_session() (workspace file
# registration etc.) — so the worst case per process is
# 2 x (pool size + max overflow). Size that, multiplied by backend/worker
# process count, below PostgreSQL's max_connections.
# XAGENT_DB_POOL_SIZE="10"
# XAGENT_DB_MAX_OVERFLOW="20"
# Seconds to wait for a free pooled connection before raising.
# XAGENT_DB_POOL_TIMEOUT_SECONDS="30"
# Bound trace/checkpoint writes before submitting workers or acquiring DB connections.
# Per event loop; requires restart. Required events wait instead of being dropped.
# Sync mode leaves one shared pooled connection when pool size > 1; this does
# not reserve capacity against other writers or a custom smaller thread pool.
# XAGENT_TRACE_DB_MAX_INFLIGHT="4"
# Async trace writes default on: PostgreSQL uses psycopg (postgresql extra);
# file SQLite uses aiosqlite's dedicated connection thread and one writer per loop.
# Memory SQLite/custom session-only hosts retain bounded sync writes to preserve
# database identity. False restores bounded sync writes for all backends.
# Other database dialects must set XAGENT_ASYNC_TRACE_DB_ENABLED=false.
# On upgrade, reinstall dependencies for the async drivers or set this flag to
# false until they are installed; missing drivers otherwise abort startup.
# The separate async trace pool adds at most MAX_INFLIGHT connections per loop,
# with NO overflow, on top of the two engine budgets above. Multiply all pools
# by backend/worker count when sizing PostgreSQL max_connections. Payload/JSON
# preparation uses one dedicated worker per loop, after admission and before
# session creation. ORM/retention remain on-loop; this is not process isolation.
# XAGENT_ASYNC_TRACE_DB_ENABLED="true"
# Task execution leases keep one runner from executing the same task/run twice.
# The heartbeat defaults to one third of the TTL. Expired leases are recovered
# automatically by every backend process; final conditional updates ensure only
# one process wins each recovery.
# The TTL also sizes the durable task-command defer budget: a deferred command
# retries roughly once per second for max(60, 2 * TTL) deferrals before it
# fails terminally, so the default budget is 120. TTLs below 30 stay on the
# 60-deferral floor, and values below 10 (or non-integers) fall back to a
# TTL of 60, i.e. budget 120. A larger budget also widens the window in
# which a CANCEL can queue behind a deferring command in the per-task FIFO
# (see xorbitsai/xagent#1978).
# XAGENT_TASK_LEASE_TTL_SECONDS="60"
# Maximum API wait for accepted reply preparation; timeout does not cancel the reply.
# XAGENT_TASK_REPLY_WAIT_TIMEOUT_SECONDS="30"
# XAGENT_TASK_LEASE_HEARTBEAT_SECONDS="20"
# XAGENT_TASK_LEASE_RECOVERY_INTERVAL_SECONDS="20"
# XAGENT_TASK_LEASE_RECOVERY_BATCH_SIZE="100"
# Stale upload-compensation claims are reconciled by every backend process.
# The grace period prevents a live request from being recovered prematurely;
# exact conditional updates ensure only one process settles each claim. Each
# process examines at most one configured batch per polling tick.
# XAGENT_UPLOADED_FILE_RECOVERY_INTERVAL_SECONDS="60"
# XAGENT_UPLOADED_FILE_RECOVERY_STALE_SECONDS="300"
# XAGENT_UPLOADED_FILE_RECOVERY_BATCH_SIZE="100"
# How long shutdown waits (seconds) for the background orphaned temp-file sweep
# to stop after its cooperative stop flag is set. Bounds only the wait, not the
# uncancellable walk itself; raise it on very large uploads trees.
# XAGENT_TEMP_FILE_CLEANUP_SHUTDOWN_TIMEOUT_SECONDS="10"
# Per-server timeout (seconds) for MCP tool initialization during agent
# setup (connect + initialize + list-tools, including retries and cleanup).
# A server that exceeds it is skipped for that task. 0 disables the timeout,
# and with it the reclaim of abandoned MCP initializations and any limit on
# concurrent initializations of one server during agent setup and tool
# listing (the connection-test endpoint keeps its own cap).
# XAGENT_MCP_TOOL_INIT_TIMEOUT_SECONDS="60"
# Wall-clock ceiling (seconds) for one LLM call's whole retry loop. Attempt
# counting alone cannot bound how long a call holds an execution slot, because
# every attempt may consume a full request timeout. This gates whether a new
# attempt may start, so a call can still overrun it by one attempt's duration.
# Invalid or non-positive values fall back to the default.
# XAGENT_LLM_RETRY_DEADLINE_SECONDS="300"
# Attempts allowed when a provider refuses work for lack of capacity (as
# opposed to a transient fault, which keeps the model's own max_retries).
# A provider reporting itself over capacity has asked us not to grow its load,
# so replaying the identical request is the wrong response. This can only lower
# a call's ceiling, never raise it above max_retries.
# Invalid or non-positive values fall back to the default.
# XAGENT_LLM_CAPACITY_MAX_ATTEMPTS="2"
# Number of consecutive streaming deltas with no content, no tool-call bytes,
# no reasoning text and no finish_reason at which the stream is abandoned as
# making no progress (the Nth empty delta in a row triggers it). 0 disables
# the check. Invalid or negative values fall back to the default.
# XAGENT_LLM_STREAM_EMPTY_DELTA_LIMIT="200"
# Trailing characters inspected for a degenerate stream tail: a
# whitespace-only or periodic (repeated) tail in reasoning text, and the
# trailing-whitespace budget allowed after a complete tool-call JSON object.
# 0 disables those checks; non-whitespace after a complete object is still
# rejected because it can never parse. Invalid or negative values fall back to
# the default.
# XAGENT_LLM_STREAM_DEGENERATE_WINDOW="256"
# Longest repeat period (characters) the periodicity check searches for
# inside the degenerate-tail window above. Invalid or non-positive values
# fall back to the default.
# XAGENT_LLM_STREAM_DEGENERATE_MAX_PERIOD="64"
# Comma-separated allowlist of models for the opt-in no-payload wall-clock
# abort below. Matching is exact and case-sensitive against the model string
# sent on the wire (e.g. "moonshotai.kimi-k2.5"); no normalization or prefix
# matching. Empty means no models are opted in, and the timeout setting below
# has no effect for any model.
# XAGENT_LLM_STREAM_NO_PAYLOAD_ABORT_MODELS=""
# Seconds after the first streamed chunk before an allow-listed model's
# stream is abandoned if it has produced no content and no tool-call chunk
# in that time. Only applies to models on the allowlist above. Invalid or
# non-positive values fall back to the default.
# XAGENT_LLM_STREAM_NO_PAYLOAD_TIMEOUT_SECONDS="30"
# ===========================================
# Hot Path Cache Configuration
# ===========================================
# Enables short-TTL cache for high-frequency task/agent/model read paths.
# Docker Compose deployments start a Redis service and set
# XAGENT_REDIS_URL=redis://redis:6379/0 automatically.
#
# For local backend runs outside Docker Compose, uncomment these if Redis is
# running on your machine, for example via `brew services start redis`.
XAGENT_REDIS_URL="redis://localhost:6379/0"
# XAGENT_HOT_PATH_CACHE_ENABLED="true"
# Default TTL for agent/model response cache entries.
# XAGENT_HOT_PATH_CACHE_TTL_SECONDS="30"
# Default TTL for task polling cache entries.
# XAGENT_HOT_PATH_TASK_CACHE_TTL_SECONDS="30"
# ===========================================
# Background Jobs / Triggers Configuration
# ===========================================
# Enables Celery-backed durable background jobs for trigger processing and
# knowledge-base ingestion. Agent execution is intentionally not routed through
# Celery.
#
# Docker Compose starts worker/scheduler services and sets these automatically.
# Redis DB 0 is reserved for hot-path cache. Redis DB 1 is the Celery broker.
# XAGENT_CELERY_ENABLED="true"
# XAGENT_CELERY_BROKER_URL="redis://localhost:6379/1"
# Background job status/result is stored in the database, so a Celery result
# backend is normally unnecessary. If enabled, keep it separate from DB 0/1.
# XAGENT_CELERY_RESULT_BACKEND="redis://localhost:6379/2"
# Broker visibility timeout for long-running KB jobs.
# XAGENT_BACKGROUND_JOB_VISIBILITY_TIMEOUT_SECONDS="3600"
# Default max attempts for newly-created durable jobs.
# XAGENT_BACKGROUND_JOB_MAX_RETRIES="3"
# Longest a non-terminal job may go without a durable row update (progress
# or status persistence) before the scheduler requeues it. Not a runtime
# limit: a job that keeps persisting progress is never requeued for age.
# XAGENT_BACKGROUND_JOB_STALE_SECONDS="7200"
# Scheduler interval for scanning stale background jobs.
# XAGENT_BACKGROUND_JOB_SWEEP_INTERVAL_SECONDS="300"
# Public share-channel upload orphan GC (#973). A task-less public-share upload
# is bound to its task at run start; if the guest never finishes task creation
# it stays orphaned. An in-app background loop (runs in every deployment, no
# Celery required) reaps rows older than the TTL that still carry the
# task-less-share marker and have no task_id.
# Per-tick load: up to 20 keyset pages x 500 rows (10,000 rows, each with a
# durable-object + local-file delete), one short DB session per page. While a
# backlog remains the loop continues after a ~1s breather instead of sleeping,
# so drain throughput does not depend on the interval below; the interval only
# sets how often an already-drained backlog is re-checked.
# XAGENT_TASKLESS_UPLOAD_TTL_SECONDS="172800" # 48h; GC-eligible age
# XAGENT_ORPHAN_UPLOAD_SWEEP_INTERVAL_SECONDS="3600" # idle re-check interval
# Task deletion commits its rows first and releases the workspace directory and
# runtime-extension state afterwards. What it could not release is recorded in
# task_cleanup_obligations and retried by an in-app driver with per-row
# exponential backoff; after the attempt budget an obligation is left in the
# terminal "exhausted" state for an operator (`xagent retention cleanup-pending`).
# XAGENT_TASK_CLEANUP_RETRY_INTERVAL_SECONDS="300" # idle re-check interval
# XAGENT_TASK_CLEANUP_MAX_ATTEMPTS="8" # attempts before "exhausted"
# Workforce builder preview runs ("test before save", workforce_id NULL) are
# only invalidated client-side; an abandoned tab/browser crash/network drop
# leaves one active server-side forever. The scheduled sweep (piggybacks on
# the existing trigger-scan Celery Beat tick) cancels runs non-terminal past
# this age.
# XAGENT_WORKFORCE_PREVIEW_RUN_STALE_SECONDS="7200" # 2h; GC-eligible age
# Backend dispatcher for prepared trigger runs. Celery scans scheduled triggers,
# but agent execution is started by backend processes.
# XAGENT_TRIGGER_DISPATCHER_ENABLED="true"
# XAGENT_TRIGGER_DISPATCHER_INTERVAL_SECONDS="5"
# XAGENT_TRIGGER_DISPATCHER_BATCH_SIZE="20"
# Random delay (0-N seconds) before this backend's dispatcher first tick,
# pushing it past the likely egress-network warm-up window after a container
# restart and staggering first ticks across replicas. Does NOT limit or pace
# one backend's first-tick backlog -- that backlog (still governed by the
# scan/batch settings above) fires in full once the delay elapses. 0 disables
# the delay.
# XAGENT_TRIGGER_DISPATCHER_STARTUP_JITTER_SECONDS="30"
# Rate limits for the public trigger callback endpoint (keyed by callback id +
# caller IP) and trigger create/update/delete APIs (keyed by user). Uses Redis
# when XAGENT_REDIS_URL is set; otherwise limits are per backend process.
# XAGENT_TRIGGER_CALLBACK_RATE_LIMIT="120/minute"
# Per-IP ceiling across all callback ids (blocks rotating-id floods).
# XAGENT_TRIGGER_CALLBACK_IP_RATE_LIMIT="600/minute"
# XAGENT_TRIGGER_CRUD_RATE_LIMIT="60/minute"
# Public share-channel abuse controls (#973). Same Redis-backed limiter; all
# are rate strings in the limits notation. Auth is keyed per share token +
# per caller IP; task-create per guest + per share token; ws turns and uploads
# per guest; ws connection attempts (pre-auth, so no guest exists yet) per
# caller IP. The run quotas cap owner-billed runs a link/guest can start per
# rolling window so one public link cannot drain the owner's team quota.
# XAGENT_SHARE_AUTH_RATE_LIMIT="60/minute"
# XAGENT_SHARE_AUTH_IP_RATE_LIMIT="300/minute"
# XAGENT_SHARE_TASK_CREATE_RATE_LIMIT="30/minute"
# XAGENT_SHARE_TASK_CREATE_TOKEN_RATE_LIMIT="120/minute"
# XAGENT_SHARE_WS_TURN_RATE_LIMIT="60/minute"
# XAGENT_SHARE_WS_CONNECT_IP_RATE_LIMIT="120/minute"
# XAGENT_SHARE_UPLOAD_RATE_LIMIT="60/minute"
# Widget uploads are keyed per widget entity + per caller IP instead of per
# guest: the widget guest_id is client-supplied (rotatable), unlike the
# server-minted share guest_id.
# XAGENT_WIDGET_UPLOAD_RATE_LIMIT="240/minute"
# XAGENT_WIDGET_UPLOAD_IP_RATE_LIMIT="60/minute"
# Widget websocket abuse controls (#1056): connection attempts per caller IP
# (pre-auth, refused pre-accept when over budget) and run-starting turns per
# caller IP (tight) + per widget entity (loose backstop). Separate buckets
# from the share websocket limits so the two public channels cannot consume
# each other's budgets.
# XAGENT_WIDGET_WS_CONNECT_IP_RATE_LIMIT="120/minute"
# XAGENT_WIDGET_WS_TURN_IP_RATE_LIMIT="60/minute"
# XAGENT_WIDGET_WS_TURN_RATE_LIMIT="240/minute"
# Widget HTTP/quota abuse controls (#1108): the remaining share-path controls
# with no widget counterpart. Every widget gate uses the tight-per-IP /
# loose-per-entity pairing: the caller IP is the per-visitor/per-abuser bound
# (the widget guest_id is client-supplied, so it is never a limiter key), and
# the per-entity bucket is a loose aggregate backstop across one embedded
# agent/workforce's visitors. Auth + embed-ticket minting share the same
# limit *configuration* and the same per-IP counter, but their per-entity
# counters are disjoint (embed-ticket keys the entity bucket on the widget
# key; auth keys it on the owner entity from the ticket's signed claims), so
# the two env values below are shared budgets only in the per-IP dimension.
# The auth entity default is deliberately loose (1200/min, a 4:1 entity:IP
# ratio matching the sibling widget gates) because both endpoints fire on
# every page load and the entity is shared by all a widget's visitors — a
# tight per-entity bucket would 429 ordinary visitors on a busy embed, and
# an auth denial is fail-closed client-side (the widget never loads). The
# widget run quota caps owner-billed runs per rolling window with a
# per-creating-IP sub-quota (keyed per widget, so exhausting it on one embed
# never blocks the same network on another); NOTE it applies to already-live
# widget tasks as soon as it deploys, and it is charged per conversation turn,
# not once per task. Separate buckets from the share limits.
# NAT / shared egress: every per-IP bucket below is shared by all genuine
# visitors behind one address — a corporate NAT or a carrier CGNAT can put
# thousands of people on one IP — so raise the per-IP values for deployments
# fronted by large shared egress. CAUTION: every per-IP bucket also needs
# XAGENT_TRUSTED_PROXY_HOPS set correctly behind a reverse proxy — with 0
# hops, all traffic resolves to the proxy's own IP and a per-IP cap silently
# becomes one global cap for the deployment; set it too high and the client
# controls the derived IP through X-Forwarded-For.
# XAGENT_WIDGET_AUTH_RATE_LIMIT="1200/minute"
# XAGENT_WIDGET_AUTH_IP_RATE_LIMIT="300/minute"
# XAGENT_WIDGET_TASK_CREATE_RATE_LIMIT="240/minute"
# XAGENT_WIDGET_TASK_CREATE_IP_RATE_LIMIT="60/minute"
# XAGENT_WIDGET_RUN_QUOTA="500/day"
# XAGENT_WIDGET_RUN_IP_QUOTA="120/hour"
# XAGENT_SHARE_RUN_QUOTA="500/day"
# XAGENT_SHARE_RUN_GUEST_QUOTA="60/hour"
# Behind a reverse proxy, set the number of trusted proxy hops so the caller
# IP for rate limiting is read from X-Forwarded-For. 0 = use the peer address.
# IMPORTANT: when the backend sits behind a reverse proxy / load balancer and
# this stays at 0, every request appears to come from the proxy's IP, so every
# per-IP limit (share/widget auth, task-create, the widget run IP sub-quota,
# trigger callbacks) silently collapses into a single global cap across ALL
# callers and can hard-429 legitimate traffic. Set it to the number of proxies
# you control in front of the backend (e.g. 1 for a single nginx/ingress hop —
# the bundled docker-compose.yml already sets 1 for its nginx). Do NOT set it
# HIGHER than the real count: X-Forwarded-For is append-order, so an over-count
# reads a client-supplied entry and lets the caller spoof the derived IP —
# which now also picks a persisted quota key (widget_client_ip). Match it to
# your real topology exactly.
# XAGENT_TRUSTED_PROXY_HOPS="0"
# Gmail incoming-email triggers (per-mailbox Pub/Sub provisioning).
#
# Credentials: the backend uses Application Default Credentials (ADC). Either
# run on GCP with an attached service account, or set
# GOOGLE_APPLICATION_CREDENTIALS to a service-account JSON key file. The
# service account needs roles/pubsub.editor (create/delete topics and
# subscriptions) on the project below, and each per-mailbox topic must allow
# gmail-api-push@system.gserviceaccount.com to publish
# (roles/pubsub.publisher) - granted automatically during provisioning.
#
# Master switch for the Gmail watch feature: gates watch registration (on
# OAuth connect and on Gmail trigger create/update/enable) as well as the
# background renewal and retry scans. While "false" (the default), Gmail
# triggers report a failed provisioning status with an explicit disabled
# error and no new watches are registered. Disabling this flag does not stop
# an existing Gmail watch: callbacks can remain deliverable until that watch
# expires or its mailbox resources are explicitly torn down.
# Teardown-on-unbind is deliberately ungated: rebinding, disabling, or
# deleting a Gmail trigger still releases the old mailbox's watch/Pub/Sub
# resources while this flag is off.
# The operator endpoint-reconciliation CLI (reconcile_gmail_push_endpoints)
# is also deliberately ungated, so endpoints can be migrated before enabling
# this flag.
# XAGENT_GMAIL_WATCH_ENABLED="false"
# XAGENT_GMAIL_PUBSUB_PROJECT_ID="your-gcp-project"
# Deterministic per-mailbox resource names: {prefix}-{mailbox-hash}.
# XAGENT_GMAIL_PUBSUB_TOPIC_PREFIX="xagent-gmail"
# XAGENT_GMAIL_PUBSUB_SUBSCRIPTION_PREFIX="xagent-gmail-push"
# Pub/Sub client transport: "grpc" (default) or "rest". Use "rest" behind
# egress proxies that cannot tunnel gRPC (the gRPC channel hangs there and
# Gmail provisioning stays pending without an error).
# XAGENT_GMAIL_PUBSUB_TRANSPORT="grpc"
# Canonical public base URL of this backend API. Browser-facing and MCP OAuth
# flows use this base. This is NOT the frontend URL (XAGENT_APP_BASE_URL).
# When unset, MCP OAuth
# redirect URIs fall back to XAGENT_APP_BASE_URL — that only works when the
# frontend origin proxies /api/* to this backend (e.g. the nginx compose
# setup), so split-port local development must set this explicitly.
# XAGENT_PUBLIC_API_BASE_URL="https://api.example.com"
# Optional backend URL advertised to server-to-server Gmail Pub/Sub and A2A
# clients. Regional deployments can set their direct origin here; when unset,
# it falls back to XAGENT_PUBLIC_API_BASE_URL for backward compatibility.
# XAGENT_S2S_API_BASE_URL="https://region-origin.example.com"
# Deprecated Gmail-only fallback retained for existing deployments. Prefer
# XAGENT_S2S_API_BASE_URL for new regional configurations.
# XAGENT_TRIGGER_CALLBACK_BASE_URL="https://legacy-callback.example.com"
# Service-account email whose OIDC identity signs push deliveries.
# XAGENT_GMAIL_PUBSUB_PUSH_SERVICE_ACCOUNT="pubsub-push@your-gcp-project.iam.gserviceaccount.com"
# Trigger create/update returns a pending registration after this many seconds
# while provisioning converges in the background.
# XAGENT_GMAIL_REGISTRATION_TIMEOUT_SECONDS="10"
# XAGENT_GMAIL_WATCH_RENEWAL_INTERVAL_SECONDS="3600"
# XAGENT_GMAIL_WATCH_RENEWAL_LEAD_SECONDS="86400"
# ===========================================
# LLM API Keys
# ===========================================
# OPENAI_API_KEY="your-openai-api-key"
INFERENCE_API_KEY="your-inference-api-key"
# DEEPSEEK_API_KEY="your-deepseek-api-key"
# Optional DeepSeek defaults
# DEEPSEEK_MODEL_NAME="deepseek-v4-flash"
# Official default base URL does not use the legacy /v1 suffix
# DEEPSEEK_BASE_URL="https://api.deepseek.com"
# DEEPSEEK_REASONING_EFFORT="high"
# ElevenLabs speech (ASR/TTS)
# ELEVENLABS_API_KEY="your-elevenlabs-api-key"
# ELEVENLABS_BASE_URL="https://api.elevenlabs.io"
# OpenRouter official-provider pinning is disabled by default to preserve
# OpenRouter fallback behavior. Set to true to route official model families
# (OpenAI, Anthropic, Google, DeepSeek, MiniMax, Z.AI) only to official endpoints.
# XAGENT_OPENROUTER_OFFICIAL_PROVIDERS_ONLY="false"
# Comma-separated exact, case-sensitive OpenRouter model slugs to omit from
# xrouter candidate sets. This does not disable profile lookup, explicit model
# selection, or XAGENT_ROUTER_FALLBACK_MODEL.
# XAGENT_XROUTER_EXCLUDED_MODELS="z-ai/glm-5.3-flash"
# When the user answers a form the agent asked for with ask_user_question, the
# resumed turn tells the model that the reply answers that form and to use it
# to continue the current request. On by default for all models; set to false
# to disable.
# XAGENT_FORM_ANSWER_CONTINUATION_ENABLED=true
# ===========================================
# Embedding API Keys (for vector memory)
# ===========================================
DASHSCOPE_API_KEY="your-dashscope-api-key"
OPENAI_EMBEDDING_API_KEY="your-openai-api-key"
# Memory Store Configuration
# Optional: Override automatic detection
# MEMORY_STORE_TYPE=in_memory|lancedb
# MEMORY_LANCEDB_DIR=./memory_store
#
# Auto-run LanceDB user_id migration on startup (default: false)
# Set to true to run background backfill when startup detects missing user_id fields.
# LANCEDB_AUTO_MIGRATE=true
# Milvus Vector Store Provider (optional, provider layer only)
# Used by xagent.providers.vector_store.milvus.get_client_from_env()
# Install dependency first: pip install pymilvus
# MILVUS_URI="http://localhost:19530"
# MILVUS_TOKEN=""
# MILVUS_DB_NAME=""
# ===========================================
# DeepDoc Remote Inference (Xinference)
# ===========================================
# Offload DeepDoc PDF parsing (OCR/layout/table models) to a remote Xinference
# server with a GPU, using the DeepDoc model's task="parse" whole-document
# pipeline. When set, PDFs are parsed remotely; every other format, and every
# PDF when this is unset, is parsed locally. On remote failure xagent logs a
# warning and falls back to local parsing automatically.
# XAGENT_DEEPDOC_XINFERENCE_URL="http://gpu-host:9997"
# UID of the launched DeepDoc model to target (default: DeepDoc).
# XAGENT_DEEPDOC_XINFERENCE_MODEL_UID="DeepDoc"
# API key, sent as the bearer token; falls back to XINFERENCE_API_KEY if unset.
# XAGENT_DEEPDOC_XINFERENCE_API_KEY=""
# Alternative to the API key: credentials exchanged for a JWT at POST /token.
# When both are set, these win. Leave all of them unset for a cluster that runs
# without authentication.
# XAGENT_DEEPDOC_XINFERENCE_USERNAME=""
# XAGENT_DEEPDOC_XINFERENCE_PASSWORD=""
# Request timeout in seconds for one whole-document parse (default: 1800).
# XAGENT_DEEPDOC_XINFERENCE_TIMEOUT_SECONDS="1800"
# ===========================================
# Other Configuration
# ===========================================
# JWT Authentication (required for production)
# Generate a secure secret with:
# python -c "import secrets; print(secrets.token_urlsafe(48))"
XAGENT_JWT_SECRET="replace-with-a-long-random-secret"
XAGENT_JWT_ALGORITHM="HS256"
# Access token expiry in minutes (default: 120)
XAGENT_ACCESS_TOKEN_EXPIRE_MINUTES="120"
# Refresh token expiry in days (default: 7)
XAGENT_REFRESH_TOKEN_EXPIRE_DAYS="7"
# Minimum password length for setup/register/change-password (default: 6)
XAGENT_PASSWORD_MIN_LENGTH="6"
# Password reset link expiry in minutes (default: 30)
# XAGENT_PASSWORD_RESET_EXPIRE_MINUTES="30"
# Frontend base URL used in reset-password emails
# XAGENT_APP_BASE_URL="http://localhost:3000"
# Slack workspace OAuth (optional).
# Create one distributed Slack app, enable Socket Mode and configure the bot
# scopes/events shown on the Channels page. The Redirect URL registered in
# Slack must exactly match XAGENT_SLACK_REDIRECT_URI. If the explicit redirect
# is omitted, Xagent derives it as:
# {XAGENT_PUBLIC_API_BASE_URL}/api/channels/slack/oauth/callback
# Keep Slack token rotation disabled; this integration stores the workspace's
# non-expiring bot token returned by oauth.v2.access.
# Socket Mode uses one shared app-level token (connections:write) and routes
# workspace events by team_id; users never need to paste xoxb/xapp tokens.
# The Slack app must subscribe to these bot events: app_mention,
# message.channels, message.groups, message.im, message.mpim, plus
# app_uninstalled and tokens_revoked so Xagent can deactivate the channel
# when a workspace removes the app.
# Workspace tokens are encrypted at rest with ENCRYPTION_KEY (see the security
# section); the OAuth install flow refuses to start while ENCRYPTION_KEY is
# unset or still the built-in dev default. Generate one with:
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# XAGENT_SLACK_CLIENT_ID="123456789.123456789"
# XAGENT_SLACK_CLIENT_SECRET="your-slack-client-secret"
# XAGENT_SLACK_APP_TOKEN="xapp-your-app-level-token"
# XAGENT_SLACK_REDIRECT_URI="https://api.example.com/api/channels/slack/oauth/callback"
# ===========================================
# SMTP Configuration (for password reset email)
# ===========================================
# XAGENT_SMTP_HOST="smtp.example.com"
# XAGENT_SMTP_PORT="587"
# XAGENT_SMTP_USERNAME=""
# XAGENT_SMTP_PASSWORD=""
# XAGENT_SMTP_USE_TLS="true"
# XAGENT_SMTP_USE_SSL="false"
# XAGENT_SMTP_FROM_EMAIL="no-reply@example.com"
# XAGENT_SMTP_FROM_NAME="Xagent"
# Google OIDC login (optional)
# Configure these to enable "Continue with Google" on the web login page.
# XAGENT_GOOGLE_OIDC_CLIENT_ID="your-google-oauth-client-id"
# XAGENT_GOOGLE_OIDC_CLIENT_SECRET="your-google-oauth-client-secret"
# XAGENT_GOOGLE_OIDC_REDIRECT_URI="http://localhost:8000/api/auth/oidc/google/callback"
# XAGENT_FRONTEND_URL="http://localhost:3000"
# XAGENT_SESSION_SECRET="change-me-to-a-random-session-secret"
# XAGENT_OIDC_LOGIN_TTL_SECONDS="600"
# XAGENT_OIDC_EXCHANGE_TTL_SECONDS="120"
# MCP OAuth local development only.
# Set XAGENT_PUBLIC_API_BASE_URL above to the externally reachable backend
# origin used for automatic {base}/api/mcp/oauth/callback redirect URIs.
# Keep disabled in production; enabling this allows OAuth discovery/token URLs
# to target localhost, private, link-local, reserved, multicast, or unspecified
# addresses for local authorization-server testing.
# XAGENT_MCP_OAUTH_ALLOW_PRIVATE_HOSTS="false"
# Trusted Toby actor-scoped stdio execution. Keep disabled until rollout.
# XAGENT_TOBY_PERSONAL_STDIO_ENABLED="false"
# Optional explicit trusted proxy for outbound MCP OAuth discovery/token calls.
# System HTTP_PROXY/HTTPS_PROXY environment variables are intentionally ignored.
# XAGENT_MCP_OAUTH_PROXY_URL="http://proxy.example.com:8080"
# Trust an ambient HTTP_PROXY/HTTPS_PROXY for outbound public-network fetches
# (webpage fetching, remote SVG inspection, web asset downloads). These paths
# validate and pin the target's DNS-resolved IP to close a DNS-rebinding /
# TOCTOU SSRF window, but a proxy performs its own, independent DNS
# resolution of the target host, reopening that window. Only enable this if
# the configured proxy itself enforces a private-range egress policy. Leave
# disabled (the default) to reject these fetches outright whenever a proxy is
# configured but not explicitly trusted.
# XAGENT_TRUSTED_EGRESS_PROXY="false"
# Image processing
CONCURRENT=5
# Langfuse Tracing
LANGFUSE_TRACING_ENABLED="true"
# Langfuse Python SDK v4 uses `base_url`; keep `LANGFUSE_HOST` only for legacy env fallback.
LANGFUSE_BASE_URL="http://127.0.0.1:3000"
LANGFUSE_PUBLIC_KEY="public key"
LANGFUSE_SECRET_KEY="secret key"
# Per-field byte cap for the LLM I/O audit trace written to trace_events
# (data.messages / data.response / etc.). Anything larger is truncated with a
# "[truncated N chars]" suffix. A long DAG task hitting all audit sites can
# otherwise write multi-MB rows. Also bounds the rendered size of every
# trace category's console log line (not just LLM audit events). Default:
# 50000 (~50KB).
# XAGENT_MAX_TRACE_PAYLOAD_BYTES="50000"
# Web Search API Keys
# Provider selection: auto, google, tavily, exa, zhipu (default: auto)
# In auto mode, priority is Zhipu > Tavily > Exa > Google.
XAGENT_WEB_SEARCH_PROVIDER="auto"
# Website import TLS fingerprint impersonation for WAF-protected sites.
# Leave empty/none for plain httpx. Use auto to try the crawler fallback chain.
# Requires installing the optional extra: pip install "xagent[waf-crawl]"
# XAGENT_WEB_CRAWL_TLS_IMPERSONATE="auto"
# In-turn tool concurrency: run independent, concurrency-safe tool calls from a
# single ReAct turn in parallel instead of one-by-one. Default off (serial).
# XAGENT_TOOL_PARALLEL_ENABLED="false"
# Max concurrent tool calls per turn batch (kept low to limit API rate-limit
# pressure). Invalid/non-positive values fall back to the default.
# XAGENT_TOOL_MAX_CONCURRENCY="3"
# Out-of-tree task runtime provider hooks use a dedicated process-wide worker
# pool so a blocking provider cannot starve unrelated asyncio.to_thread work.
# Size this for the expected cross-task/provider concurrency. Queue wait has a
# separate timeout and does not consume each hook's execution-time allowance.
# XAGENT_TASK_RUNTIME_HOOK_MAX_WORKERS="8"
# XAGENT_TASK_RUNTIME_HOOK_QUEUE_TIMEOUT_SECONDS="30"
# ===========================================
# Local browser use
# ===========================================
# Controls one visible window of the configured browser on the same host as
# the Xagent backend through `cua-driver mcp`. Tasks bind to an exact browser
# window and never switch silently. Keep disabled on shared/cloud hosts.
# XAGENT_NATIVE_BROWSER_ENABLED="false"
# XAGENT_NATIVE_BROWSER_APP_NAME="Google Chrome"
# XAGENT_BROWSER_CUA_DRIVER_COMMAND="cua-driver"
# XAGENT_BROWSER_CUA_DRIVER_SOCKET=""
# XAGENT_BROWSER_CUA_DRIVER_TIMEOUT_SECONDS="30"
# Number of AX nodes scanned before Xagent prioritizes at most 100 visible,
# actionable elements for model context. A larger scan preserves transient
# menus without sending the full accessibility tree to the model.
# XAGENT_BROWSER_CUA_DRIVER_MAX_ELEMENTS="2000"
# Fallback locale/timezone for browser_use (Playwright) sessions, used when a
# task carries no resolvable locale of its own (the app_locale cookie set by
# the web UI's language switcher). Locale defaults to "en-US"; timezone
# defaults to the host's own system timezone when unset. An invalid value
# raises on first browser-tool use (not at process startup, and not silently
# ignored) -- check the error against the tool call that triggered it, since
# the validation itself lives in src/xagent/config.py, not here.
# XAGENT_BROWSER_TOOL_DEFAULT_LOCALE="en-US"
# XAGENT_BROWSER_TOOL_DEFAULT_TIMEZONE=""
# Context compaction: the conversation is compacted when the estimated context
# size reaches (model context window * ratio). Set a model's context window in
# the model config; models without one fall back to the default below.
# Ratio must be within (0, 1]; invalid values fall back to 0.75.
# XAGENT_COMPACT_THRESHOLD_RATIO="0.75"
# Fallback threshold in tokens when a model has no context window configured.
# XAGENT_COMPACT_THRESHOLD_DEFAULT="32000"
# Checkpoint storage encoding v2: dedup nested DAG/auto contexts, per-record
# tool ledgers, and large system prompts into content-addressed blob tables.
# Decode support is unconditional; this only gates NEW writes. For a mixed
# fleet, roll out in two phases: deploy every instance with "false" first
# (decode-only), then remove the override to start writing v2. Default on.
# XAGENT_CHECKPOINT_ENCODING_V2="true"
# How many checkpoint trace-event rows to keep per task execution. Older rows
# are pruned when a new checkpoint is written (resume only reads the latest
# readable one; a few extras are kept as fallback). "0" disables pruning.
# XAGENT_CHECKPOINT_HISTORY_LIMIT="8"
# Seconds a user-message injection may hold a context's exclusive checkpoint
# section (write plus read-back) before a warning is logged and the
# xagent.agent.checkpoint_gate.exclusive.stalls counter increments. The section
# is never timed out; the warning repeats each interval while it is held.
# XAGENT_CHECKPOINT_GATE_STALL_WARNING_SECONDS="30"
# Zhipu Web Search (recommended for Chinese users, get key at https://open.bigmodel.cn/)
ZHIPU_API_KEY=""
# Tavily Search API (alternative, get key at https://tavily.com)
TAVILY_API_KEY=""
# Exa AI-powered Search (get key at https://exa.ai)
EXA_API_KEY=""
# Google Custom Search (fallback, requires Google Cloud setup)
GOOGLE_API_KEY=""
GOOGLE_CSE_ID=""
# ===========================================
# Google OAuth Configuration (Optional)
# ===========================================
# Required for Google Login and Google Drive integration
# Create credentials at https://console.cloud.google.com/apis/credentials
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# Redirect URI for Google OAuth callback (Required for Google Login)
# Example: http://localhost:8000/api/auth/google/callback
GOOGLE_REDIRECT_URI=""
# Google Drive Picker configuration (required when Drive uses the restricted
# drive.file scope). GOOGLE_PICKER_APP_ID is the numeric Google Cloud project
# number associated with GOOGLE_CLIENT_ID (or leave it blank to derive it from
# a numeric OAuth client_id). GOOGLE_PICKER_API_KEY must be a dedicated
# browser/referrer-restricted key; do not reuse GOOGLE_API_KEY. Both the
# restricted Picker key and the short-lived OAuth access token reach the
# browser; never use a server-only credential here.
GOOGLE_PICKER_API_KEY=""
GOOGLE_PICKER_APP_ID=""
# Enable only in the Google review environment until Google approves these scopes.
# true: full Drive scope and visible Gmail connector with gmail.modify.
# false (default): drive.file and hidden Gmail; keep existing Drive visibility.
XAGENT_GOOGLE_RESTRICTED_SCOPES_ENABLED=false
# Google Ads developer token (Optional)
# Required for the Google Ads connector in MCP. Apply for/find this token in
# the Ads API Center of a Google Ads manager (MCC) account:
# https://ads.google.com/aw/apicenter
# This is a single platform-wide token shared by all users who connect the
# Google Ads app (not a per-user secret), analogous to GOOGLE_CLIENT_ID above.
GOOGLE_ADS_DEVELOPER_TOKEN=""
# ===========================================
# LinkedIn OAuth Configuration (Optional)
# ===========================================
# Required for LinkedIn integration in MCP
# Create credentials at https://www.linkedin.com/developers/apps/new
LINKEDIN_CLIENT_ID=""
LINKEDIN_CLIENT_SECRET=""
# Redirect URI for LinkedIn OAuth callback
# Example: http://localhost:8000/api/auth/linkedin/callback
LINKEDIN_REDIRECT_URI=""
# ===========================================
# HubSpot MCP Connector Configuration (Optional)
# ===========================================
# Required for the hosted HubSpot MCP integration. Create an MCP Connector in
# HubSpot Development and configure this redirect URL there:
# {XAGENT_PUBLIC_API_BASE_URL}/api/mcp/oauth/callback
# HubSpot determines scopes from its hosted MCP tools and the permissions the
# installing user chooses; do not configure the legacy public-app CRM scopes.
XAGENT_HUBSPOT_MCP_CLIENT_ID=""
XAGENT_HUBSPOT_MCP_CLIENT_SECRET=""
# ===========================================
# GitHub OAuth Configuration (Optional)
# ===========================================
# Required for the GitHub integration in MCP (search repos/code, read and
# create issues and pull requests, comment, browse file contents and commit
# history). Create an OAuth App at https://github.com/settings/developers
# (no app review needed -- it works immediately for any GitHub account).
# GitHub OAuth Apps have no scope configuration step of their own -- this
# connector requests read:user repo user:email at authorize time, and
# there is nothing to set up on GitHub's side to match it.
# Note: the GITHUB_ prefix is reserved by GitHub Actions for its own
# runner-provided variables (GITHUB_TOKEN, GITHUB_SHA, etc.) -- setting
# these two as step/workflow env vars in an Actions workflow will be
# rejected. That only affects deployments that configure this service via
# an Actions workflow; a plain .env file or container env is unaffected.
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# If the OAuth App has "token expiration" enabled, tokens carry a
# refresh_token and expire in ~8 hours; refresh is handled automatically.
# To rotate the client secret: the persisted provider row (admin connector
# settings) wins over this env var for both connect and refresh once it's
# non-empty. Either update the row to the new secret, or clear the row and
# set this var instead -- both connect and refresh fall back to it when the
# row is blank, so clearing it does not break refresh for already-connected
# users.
# Redirect URI for GitHub OAuth callback -- must exactly match the
# "Authorization callback URL" configured for the app. GitHub does not
# enforce port matching for localhost/127.0.0.1, but this value is still
# used verbatim in both the authorize request and the token exchange.
# Example: http://localhost:8000/api/auth/github/callback
GITHUB_REDIRECT_URI=""
# ===========================================
# Meta OAuth Configuration (Optional)
# ===========================================
# Required for Facebook Pages, Instagram, Meta Ads, and WhatsApp Business
# integrations in MCP
# Create credentials at https://developers.facebook.com/apps/
#
# IMPORTANT for WhatsApp Business specifically: its three scopes
# (business_management, whatsapp_business_management, whatsapp_business_messaging)
# need Advanced Access on your Meta app before anyone outside the app's own
# roles (admin/developer/tester) can grant them. Before that, a non-role user
# can still complete the OAuth flow "successfully" -- Meta silently omits the
# ungranted scopes from the resulting token rather than failing the login --
# and every WhatsApp tool call then fails against that under-scoped token.
# Confirm Advanced Access (or restrict who can connect this app) before
# rolling WhatsApp Business out beyond your own app's roles.
META_CLIENT_ID=""
META_CLIENT_SECRET=""
# Redirect URI for Meta OAuth callback
# Example: http://localhost:8000/api/auth/meta/callback
META_REDIRECT_URI=""
# Optional Facebook Login for Business configuration ID. When set, Meta OAuth
# uses config_id instead of a raw scope list, which is required by some Meta app
# configurations for Pages, Instagram, and Ads permissions.
# IMPORTANT: the permission list is not sent as a `scope` param in this mode -
# it comes entirely from the Login Configuration in the Meta App Dashboard, and
# the resulting token is capable of everything that Configuration grants,
# regardless of which app (Facebook/Instagram/Meta Ads/WhatsApp Business) the
# user connected. META_CONFIG_ID is a fallback shared by every Meta app that
# has no app-specific override below - do NOT widen it to include a
# connector-specific scope (e.g. ads_read, or WhatsApp's business_management/
# whatsapp_business_management/whatsapp_business_messaging) just to cover one
# app, since that also hands that scope to every other app's tokens. Give the
# app that needs isolation its own Login Configuration containing only the
# permissions it actually needs instead.
META_CONFIG_ID=""
# Per-app overrides: when set, these take precedence over META_CONFIG_ID for
# their app's authorize request, so each can point at a Login Configuration
# scoped to just that app's own permissions.
# - Facebook Pages: pages_show_list, pages_read_engagement,
# pages_manage_posts, pages_read_user_content
META_FACEBOOK_CONFIG_ID=""
# - Instagram: pages_show_list, pages_read_engagement, instagram_basic,
# instagram_content_publish
META_INSTAGRAM_CONFIG_ID=""
# - Meta Ads: ads_read only
META_ADS_CONFIG_ID=""
# - WhatsApp Business: business_management, whatsapp_business_management,
# whatsapp_business_messaging
META_WHATSAPP_CONFIG_ID=""
# Meta Ads note: a new app only gets Standard/Limited Marketing API access,
# which reaches ad accounts your own app admins/developers already have
# access to. Reaching other advertisers' accounts needs Advanced Access to
# ads_read via Meta's App Review (and likely Business Verification) - see
# https://developers.facebook.com/docs/marketing-apis/access for current
# requirements before relying on this connector in production.
# WhatsApp Business note: see the Advanced Access caveat under META_CLIENT_ID
# above -- the same requirement applies to its three scopes here.
# ===========================================
# Slack OAuth Configuration (Optional)
# ===========================================
# Required for the Slack integration in MCP (search/read channel, thread, and
# DM history and topics; post messages and thread replies; react to
# messages; upload files; join a public channel when asked to).
# Create an app at https://api.slack.com/apps, add these bot scopes under
# "OAuth & Permissions": chat:write, chat:write.public, channels:read,
# channels:history, channels:join, groups:read, groups:history, im:read,
# im:history, mpim:read, mpim:history, reactions:write, files:write
# Note: chat:write.public also lets the bot post into any public channel
# without being invited — it bypasses Slack's invite gate.
# Leave "Token Rotation" off — bot tokens don't expire, so no refresh is needed.
# Existing connections must be reconnected after adding the new scopes above
# to an already-installed app (Slack does not retroactively grant scopes to
# tokens issued before they were added).
SLACK_CLIENT_ID=""
SLACK_CLIENT_SECRET=""
# Redirect URI for Slack OAuth callback — must also be added under the app's
# "OAuth & Permissions" > "Redirect URLs".
# Example: http://localhost:8000/api/auth/slack/callback
SLACK_REDIRECT_URI=""
# Note: slack_upload_file's allowed-directory allowlist (like the LinkedIn
# connector's equivalent) is scoped to the task workspace automatically by
# the launcher — there is no operator-facing env var for it here.
# ===========================================
# Zoom OAuth Configuration (Optional)
# ===========================================
# Required for the Zoom integration in MCP (meeting lookup, cloud recordings,
# transcripts). Create a "General App" (User Managed) at
# https://marketplace.zoom.us/develop/create and add the scopes:
# meeting:read:meeting, meeting:read:list_meetings, meeting:read:past_meeting,
# cloud_recording:read:list_recording_files, cloud_recording:read:meeting_transcript,
# user:read:user
ZOOM_CLIENT_ID=""
ZOOM_CLIENT_SECRET=""
# Redirect URI for Zoom OAuth callback — must exactly match a URL registered
# in the app's OAuth Allow List.
# Example: http://localhost:8000/api/auth/zoom/callback
ZOOM_REDIRECT_URI=""
# ===========================================
# Intercom OAuth Configuration (Optional)
# ===========================================
# Required for the Intercom integration in MCP (search contacts, review
# conversations, reply to customers). Create an app at
# https://developers.intercom.com/ and, under Authentication, enable OAuth
# and select the permissions this connector needs (Read/write contacts and
# conversations). Intercom does not accept a `scope` parameter on the
# authorize URL — granted permissions come entirely from that Authentication
# configuration, not from anything this app sends.
# One app is expected to cover every workspace region (US/EU/AU): the API
# host (api.intercom.io) auto-routing to the workspace's actual region is
# documented in Intercom's REST API reference. The authorize host
# (app.intercom.com) working the same way for a Public App comes from
# Intercom's own community answer rather than primary docs, and hasn't been
# verified against a live non-US workspace — see builtin_mcp_registry.py's
# intercom provider row for the citation. Unlike Intercom's hosted MCP
# server, which has separate per-region endpoints and no AU support at all,
# this is the reason a local connector was built instead of using that.
INTERCOM_CLIENT_ID=""
INTERCOM_CLIENT_SECRET=""
# Redirect URI for Intercom OAuth callback — must also be added under the
# app's Authentication > Redirect URLs.
# Example: http://localhost:8000/api/auth/intercom/callback
INTERCOM_REDIRECT_URI=""
# ===========================================
# Salesforce OAuth Configuration (Optional)
# ===========================================
# Required for the Salesforce integration in MCP (query/manage accounts,
# contacts, leads, opportunities, and custom objects via SOQL/SOSL). Create a
# Connected App at Setup > App Manager > New Connected App, enable OAuth
# Settings, and select the scopes: api, refresh_token (offline_access),
# openid. The Consumer Key/Secret are under the app's "Manage Consumer
# Details" after saving.
# A free Developer Edition org (https://developer.salesforce.com/signup) is
# enough to create a Connected App and test this connector.
# Note: the authorize/token URLs are hardcoded to login.salesforce.com
# (production). Sandbox orgs authenticate through test.salesforce.com
# instead; there is no env var to switch it, but an admin can hand-edit a
# second oauth_providers row (e.g. "salesforce-sandbox", so both coexist)
# to point at test.salesforce.com, plus a matching second public_mcp_apps
# row with its own app_id/launch_config/env_mapping -- one oauth_providers
# row alone isn't a working connector. Unlike that row's own unused
# userinfo_url column, salesforce.py's USERINFO_URL is a *module-level
# constant* hardcoded to the production host and used unconditionally by
# salesforce_get_current_user on every call -- a sandbox-issued token
# sent there will fail, not silently work. There is currently no way to
# make that one tool sandbox-aware without a code change.
# Newer orgs enforce PKCE on this grant with no per-app opt-out, so the
# login route encrypts the PKCE verifier with ENCRYPTION_KEY (see the
# security section) before embedding it in the signed state token --
# unlike Slack's workspace tokens, this is the first *login-path* use of
# ENCRYPTION_KEY in this codebase, so double check it's actually set
# (not just left to the built-in development-only default) before relying
# on this connector outside local development.
SALESFORCE_CLIENT_ID=""
SALESFORCE_CLIENT_SECRET=""
# Redirect URI for Salesforce OAuth callback — must exactly match a URL
# registered under the Connected App's OAuth Settings > Callback URL.
# Example: http://localhost:8000/api/auth/salesforce/callback
SALESFORCE_REDIRECT_URI=""
# ===========================================
# Deputy OAuth Configuration (Optional)
# ===========================================
# Required for the Deputy integration in MCP (look up employees, view
# rosters/shifts, read timesheets, and create or update records such as
# employees, rosters, timesheets, and leave). Register an OAuth client at
# https://once.deputy.com/my/oauth_clients ("New Oauth Client") to get a
# Client ID/Secret. A free trial account at https://www.deputy.com includes
# a dedicated installation you can use to test this connector.
# Deputy has no fixed API host: each install's actual base URL (e.g.
# acme.au.deputy.com) is returned in the token response as `endpoint` and
# persisted per-connection as UserOAuth.instance_url -- token refresh must
# also go through that same per-install host (once.deputy.com only serves
# the initial code exchange), which tools/config.py's refresh_oauth_token_
# if_needed handles automatically once a connection exists.
# Deputy also has no granular OAuth scopes (developer.deputy.com/docs/
# using-oauth-20) -- reads and writes run at whatever permission level the
# connected account has in Deputy, not something this app can restrict.
DEPUTY_CLIENT_ID=""
DEPUTY_CLIENT_SECRET=""
# Redirect URI for Deputy OAuth callback — must exactly match the URL
# registered on the OAuth client.
# Example: http://localhost:8000/api/auth/deputy/callback
DEPUTY_REDIRECT_URI=""
# ===========================================
# MYOB OAuth Configuration (Optional)
# ===========================================
# Required for the MYOB AccountRight integration in MCP (look up contacts,
# invoices, purchase bills, and general ledger accounts). Register an
# app at https://developer.myob.com to get a Key/Secret. MYOB_CLIENT_ID
# doubles as the x-myobapi-key header every myob_*.py tool call sends
# (forwarded via launch_config.static_env, not env_mapping) -- it must be
# set here even if client_id/client_secret are also configured via the
# admin UI's provider settings, since static_env only ever reads the host
# process env, with no database fallback.
MYOB_CLIENT_ID=""
MYOB_CLIENT_SECRET=""
# Redirect URI for MYOB OAuth callback — must exactly match the URL
# registered on the app.
# Example: http://localhost:8000/api/auth/myob/callback
MYOB_REDIRECT_URI=""
# ===========================================
# Xero OAuth Configuration (Optional)
# ===========================================
# Configure a Xero authorization-code app at https://developer.xero.com/app/manage.
# These identify the OAuth application, not a user's bearer token.
# On existing deployments, also configure the Xero catalog/provider via the admin UI.
XERO_CLIENT_ID=""
XERO_CLIENT_SECRET=""
# Must exactly match the callback URL registered for the Xero app.
# Example: https://your-domain.com/api/auth/xero/callback
XERO_REDIRECT_URI=""
# ===========================================
# Linear OAuth Configuration (Optional)
# ===========================================
# Required for the Linear integration in MCP (search/manage issues, teams,
# labels, comments, and projects). Create an OAuth application at
# https://linear.app/settings/api/applications (no app review needed — it
# works immediately for any Linear workspace). Linear's app registration has
# no scope-selection field; the read/write scopes this integration needs are
# requested automatically at connect time, nothing to configure here.
LINEAR_CLIENT_ID=""
LINEAR_CLIENT_SECRET=""
# Redirect URI for Linear OAuth callback — must exactly match the
# "Callback URLs" configured for the application.
# Example: http://localhost:8000/api/auth/linear/callback
LINEAR_REDIRECT_URI=""
# ===========================================
# Employment Hero OAuth Configuration (Optional)
# ===========================================
# Required for the Employment Hero integration in MCP (look up
# organisations, employees, teams, and timesheet entries). Create an app at
# https://secure.employmenthero.com/app/v2/organisations/<org_id>/developer_portal/api
# and select the scopes: organisations:list, employees:list, employees:show,
# teams:list, timesheet_entries:list. API access requires a Platinum
# subscription or above.
# Employment Hero mandates PKCE on this grant for every app from
# 2026-09-14 — the login route always sends a code_challenge/code_verifier
# for this provider (see requires_pkce in oauth_provider_quirks.py), same
# handling as the Salesforce row above, and for the same reason: no
# per-app way to opt out. Employment Hero's token endpoint also expects a
# non-standard wire format: grant_type and redirect_uri (or refresh_token
# on refresh) go in the query string, not the form body -- handled by the
# matches_provider_family(provider, "employment-hero") branches in
# api/auth.py and tools/config.py, not anything to configure here.
EMPLOYMENT_HERO_CLIENT_ID=""
EMPLOYMENT_HERO_CLIENT_SECRET=""
# Redirect URI for Employment Hero OAuth callback — must exactly match a URL
# registered for the app in the developer portal.
# Example: http://localhost:8000/api/auth/employment-hero/callback
EMPLOYMENT_HERO_REDIRECT_URI=""
# Note: