# =========================================== # 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" # Optional isolated SQL tool service. When URL is set, SQL statements execute # only in the authenticated worker instead of the Agent process. Production # deployments should set both values and run the worker without DATABASE_URL, # Redis/cloud credentials, Docker access, or application storage mounts. # XAGENT_SQL_WORKER_URL="http://sql-worker:8090" # XAGENT_SQL_WORKER_TOKEN="" # The settings below apply in the worker process. The Agent should use the same # timeout so its request deadline is slightly longer than the worker deadline. # XAGENT_SQL_WORKER_HOST="0.0.0.0" # XAGENT_SQL_WORKER_PORT="8090" # XAGENT_SQL_WORKER_QUERY_TIMEOUT_SECONDS="30" # XAGENT_SQL_WORKER_MAX_ROWS="10000" # XAGENT_SQL_WORKER_MAX_RESULT_BYTES="8388608" # XAGENT_SQL_WORKER_MAX_QUERY_BYTES="1048576" # XAGENT_SQL_WORKER_MAX_CONCURRENCY="4" # 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" # Bounded same-identity retries for V2 execution-event writes on transient # database errors (deadlocks, lock timeouts, dropped connections). SQLite gets # at most one retry: it reports BUSY only after waiting out busy_timeout. # 1 disables retries. # XAGENT_DB_WRITE_RETRY_ATTEMPTS="3" # XAGENT_DB_WRITE_RETRY_BASE_DELAY_MS="50" # XAGENT_DB_WRITE_RETRY_MAX_DELAY_MS="1000" # 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" # Task auto-recovery. Interruptions caused by infrastructure failures (expired # leases, persistence failures, unavailable model providers) pause eligible # tasks so they can be resumed instead of failing them; so does unusable model # output, but only while automatic resume is on to sample the model again. # Master switch for pausing (instead of failing) on infrastructure failures. # Default: follows XAGENT_TASK_AUTO_RESUME_ENABLED (also when blank or # invalid). Set it to "false" to fail such runs as before; set it to "true" # with auto-resume off to keep them paused for a manual resume. Lease-expiry # recovery pauses regardless. # XAGENT_TASK_INFRA_FAILURE_PAUSE_ENABLED="true" # Resume paused tasks automatically. When false, lease-expiry pauses wait for # a manual resume and, unless the switch above is set to "true", other # interruptions fail instead of pausing. # XAGENT_TASK_AUTO_RESUME_ENABLED="true" # Sweeper polling interval, per-tick dispatch cap and global in-flight cap. # Every process that runs lease recovery runs a sweeper and each reads the # in-flight count before dispatching, so the cap is approximate: up to # MAX_INFLIGHT + (number of processes x MAX_PER_TICK) can briefly be in flight. # Turning auto-resume off stops already-scheduled resumes (they wait for a # manual resume) instead of freezing them. # XAGENT_TASK_AUTO_RESUME_POLL_SECONDS="5" # XAGENT_TASK_AUTO_RESUME_MAX_PER_TICK="20" # XAGENT_TASK_AUTO_RESUME_MAX_INFLIGHT="50" # Staleness windows: tasks interrupted longer ago than this are not resumed. # XAGENT_TASK_AUTO_RESUME_WINDOW_SECONDS="86400" # The channel window also releases a shared-channel reply still waiting for # an automatic resume that has not handed off: past it (counted from the # latest interruption, plus 60s of clock grace) the channel is told the run # was interrupted. It does not bound the whole wait: a resume already in # flight and its run add to it, and the channel gets a "still being # processed" notice after the reply wait timeout. # XAGENT_TASK_AUTO_RESUME_CHANNEL_WINDOW_SECONDS="1800" # Absolute cap on automatic resumes of a single run. # XAGENT_TASK_AUTO_RESUME_MAX_TOTAL_PER_RUN="20" # lease_expired / persistence_failure: first backoff (doubles up to 60s) and # the number of resumes allowed without progress. # XAGENT_TASK_AUTO_RESUME_SHORT_INITIAL_BACKOFF_SECONDS="10" # XAGENT_TASK_AUTO_RESUME_SHORT_MAX_NO_PROGRESS="3" # llm_unavailable: first backoff, backoff ceiling and total retry duration. # XAGENT_TASK_AUTO_RESUME_LLM_INITIAL_BACKOFF_SECONDS="60" # XAGENT_TASK_AUTO_RESUME_LLM_MAX_BACKOFF_SECONDS="900" # XAGENT_TASK_AUTO_RESUME_LLM_MAX_ELAPSED_SECONDS="7200" # model_output_invalid: run-level retries allowed without progress. # XAGENT_TASK_AUTO_RESUME_MODEL_OUTPUT_MAX_NO_PROGRESS="2" # Treat a host shutdown like a crash. When true (and auto-resume is on), a run # the shutdown cancelled keeps its lease instead of failing: lease recovery # pauses it once the lease expires and auto-resume continues it. User cancels, # channel tasks, SDK tasks and the sources auto-resume excludes (a2a, # external, widget, shared_link) still fail immediately. Resuming re-issues the LLM call # that was interrupted. A run stopped inside a tool call is left for a manual # resume (task_auto_recovery state_detail "tool_in_flight") so the call is not # repeated automatically. # XAGENT_SHUTDOWN_HANDOFF_ENABLED="false" # Per-process cap on the task executions the shared command dispatcher admits # (start, resume, reply, a message that opens a new turn). Every live execution # in the process counts against it, including ones started outside the queue, # which are not held by it. While the process is at the cap, commands that # would open an execution wait in the queue; nothing is rejected. Pause and # cancel take no slot. At the cap, a cancel or pause may be applied ahead of # the task's queued commands that would open an execution and have never been # picked up, when the task has no execution running in this process. Those # commands fail once the cancel completes and changes the task's state version, # or once the pause completes or finds nothing of the task running; if the # control is rejected for any other reason, they stay queued. A command already # picked up once keeps its place. Between limit-R and limit (R is the # interactive reserve below) the same applies to batch commands and to the # commands queued behind them. Default 80. Values below 1 fall back to 80; set # a very large value to effectively disable the cap. # XAGENT_DISPATCHER_MAX_INFLIGHT_PER_PROCESS="80" # Interactive reserve for the dispatcher cap above. R (this value) of the # limit (XAGENT_DISPATCHER_MAX_INFLIGHT_PER_PROCESS) execution slots are kept # for interactive commands. Default 0, which is the behavior described above; # it applies only under shared task execution, and the effective value is # min(R, limit-1). Once limit-R tasks are active in a process, a batch command # cannot start a new execution there; an interactive command can until the # limit is reached. A command that continues a task already active in the # process needs no further slot, and pause and cancel take none. A deployment # registers the classifier that decides whether a command is interactive; the # default rule treats commands of tasks from the internal, widget and # shared_link sources (and tasks with no source) as interactive, as is a # resume_input reply to a task waiting for input. # With admission enabled, interactive commands of a team are not prioritized # inside its admission bucket. # Between limit-R and limit a cancel or pause also passes the never-claimed # batch commands of its task held at that line, and the commands queued behind # them, and settles them as it does at the cap; a command already picked up # once keeps its place. # A command of a task that waits behind a held batch command of the same task # cannot use the reserve either; commands of one task still run in order. # On the batch line a scan that finds no control, no command of a task # running in the process and no interactive command reads the whole claimable # backlog. # XAGENT_DISPATCHER_INTERACTIVE_RESERVE_PER_PROCESS="0" # 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). It also bounds the # connector probe's tool call, which falls back to 60 seconds when this is 0. # XAGENT_MCP_TOOL_INIT_TIMEOUT_SECONDS="60" # Accounts allowed to run a connector's read-only probe # (POST /api/mcp/servers/{id}/probe): the connector smoke monitor's account, # named by exact username or by email address (case-insensitive), comma # separated. Unset, nobody may run probes. Create the account before naming # it here. The route only probes servers the caller has connected, with the # one read-only tool declared for that connector in # src/xagent/web/connector_probes.py. # XAGENT_CONNECTOR_PROBE_USERS="monitor@example.com" # 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" # Configured Auto: attempts the routed candidate gets before a call that can # move to the fallback does so; the fallback keeps its own max_retries, and a # call the fallback cannot serve keeps the candidate's. Only lowers a # candidate's ceiling. Invalid or non-positive values fall back to the # default. # XAGENT_AUTO_PRIMARY_MAX_ATTEMPTS="2" # Seconds a configured Auto candidate that just failed (provider down, timeout, # quota, rejected credential, missing model; not a rate limit) is skipped in # favour of the fallback, per process. Invalid or non-positive values fall back to the # default. # XAGENT_AUTO_CANDIDATE_COOLDOWN_SECONDS="300" # 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" # =========================================== # Shared authentication storage # =========================================== # 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" # Login and password-recovery budgets are shared across API replicas via Redis. # Outside development (or with XAGENT_REQUIRE_SECURE_SECRETS=true), Redis is # required and checked at startup. Runtime outages return HTTP 503 for these # auth operations. Development can fall back to bounded local buckets. # See docs/authentication-rate-limits.md for rollout and budgets. # Positive integers; invalid/non-positive values use the documented defaults. # XAGENT_AUTH_RATE_LIMIT_WINDOW_SECONDS="900" # XAGENT_AUTH_LOGIN_IP_LIMIT="200" # XAGENT_AUTH_LOGIN_ACCOUNT_LIMIT="50" # XAGENT_AUTH_LOGIN_PAIR_LIMIT="5" # Forgot/reset share the recovery IP budget; recovery counts all requests. # XAGENT_AUTH_RECOVERY_IP_LIMIT="200" # XAGENT_AUTH_RECOVERY_EMAIL_LIMIT="50" # XAGENT_AUTH_RECOVERY_PAIR_LIMIT="5" # XAGENT_AUTH_RATE_LIMIT_MAX_LOCAL_KEYS="10000" # # Hot Path Cache Configuration (optional, shares the Redis connection above) # Enables short-TTL cache for high-frequency task/agent/model read paths. # 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" # Milvus only: purge cadence for leftover rows, scheduled through Celery Beat # (no Beat means no purge). Deletes wait twice XAGENT_BACKGROUND_JOB_STALE_SECONDS; ignored by LanceDB. # XAGENT_KB_MILVUS_CLEANUP_INTERVAL_SECONDS="3600" # Milvus only: false turns the Beat purge off without removing the Beat entry. # XAGENT_KB_MILVUS_CLEANUP_ENABLED="true" # 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" # Auto model prompt embeddings, selected by XROUTER_EMBEDDING_BACKEND: # - unset: keep the sentence-transformers backend serialized in the router # artifact; the embedding model is loaded in this process. # - "xinference": compute the embeddings on a remote OpenAI-compatible endpoint, # so this process never loads the model. The endpoint must serve the same # embedding model the router artifact was trained with. # - "sentence-transformers": load the model in this process, rebuilt with # upstream defaults (model from XROUTER_EMBEDDING_MODEL, max sequence length # 512) instead of keeping the backend serialized in the artifact. # Values are case-sensitive. Any other value makes Auto routing fail: a # text-only request goes to the Auto fallback model when one is configured, # while a request with images, audio, video or files fails even then, because # the fallback model's input modalities cannot be checked without the routing # service. # XROUTER_EMBEDDING_BACKEND="xinference" # Default: Qwen/Qwen3-Embedding-0.6B # XROUTER_EMBEDDING_MODEL="Qwen/Qwen3-Embedding-0.6B" # Must end with /v1 ("/embeddings" is appended). Default: http://127.0.0.1:9997/v1 # XINFERENCE_BASE_URL="http://embedding-host:9997/v1" # Bearer key for that endpoint. The Xinference rerank client and DeepDoc remote # inference (as its fallback key, see below) read the same variable, so all # three send it to their own endpoints. # XINFERENCE_API_KEY="" # 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 (optional). The Docker Compose add-on docker/docker-compose.milvus.yml sets # XAGENT_VECTOR_BACKEND=milvus and MILVUS_URI for you; see docker/README.milvus.md. # Without Compose, install pymilvus and set both on a new deployment: the engine is # recorded at first start and a later change is refused. # XAGENT_VECTOR_BACKEND=milvus # 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). The stock Docker image replaces # the exact placeholder below with a random secret persisted in xagent_secrets. # Direct/non-Docker production deployments must replace it explicitly. # XAGENT_ENVIRONMENT="development" # use "production" outside local development # XAGENT_REQUIRE_SECURE_SECRETS="false" # 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: 8) XAGENT_PASSWORD_MIN_LENGTH="8" # Explicit credentialed browser origins allowed to call the API. # Defaults to XAGENT_FRONTEND_URL. A wildcard is rejected. # XAGENT_CORS_ORIGINS="http://localhost:3000" # XAGENT_SESSION_SECRET="change-me-to-a-random-session-secret" # Password reset link expiry in minutes (default: 30) # XAGENT_PASSWORD_RESET_EXPIRE_MINUTES="30" # New-email confirmation code lifetime in minutes (default: 15) # XAGENT_EMAIL_CONFIRMATION_EXPIRE_MINUTES="15" # Confirmation requests per target address per auth rate-limit window (default: 5) # Independent of the password-recovery target-email budget. # XAGENT_EMAIL_CHANGE_TARGET_LIMIT="5" # 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 (password reset, email confirmation and security notices) # Changing/adding a recovery email requires SMTP; there is no unverified fallback. # =========================================== # 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_OIDC_LOGIN_TTL_SECONDS="600" # XAGENT_OIDC_EXCHANGE_TTL_SECONDS="120" # Private model endpoints are deployment-owned exceptions, not user settings. # BOTH exact origins (scheme + host + port) and permitted DNS CIDRs are required. # Default empty = public-only; paths, query strings, userinfo and wildcards are # not allowed. Every DNS answer must be public or approved; metadata/link-local, # unspecified, multicast and reserved addresses remain forbidden regardless. # XAGENT_MODEL_PRIVATE_ENDPOINTS="http://localhost:9997,http://xinference:9997" # XAGENT_MODEL_PRIVATE_CIDRS="127.0.0.1/32,::1/128,172.20.0.10/32" # Only migrated managed model clients use these exceptions. MCP, web fetches, # connectors and third-party image/audio inputs remain public-only. See # docs/model-outbound-security.md for proxy, timeout and deferred-path limits. # Upgrade: all migrated model clients ignore untrusted ambient HTTP(S)_PROXY # and connect directly by default. Proxy-only model deployments must explicitly # enable XAGENT_TRUSTED_EGRESS_PROXY after auditing the proxy's DNS/egress policy. # They never implicitly trust a proxy or fail construction solely due to it. # This does not relax fetcher/MCP/tool proxy rejection or private-host policies. # Upgrade: Zhipu web search and third-party reference-image/audio downloads # REJECT a configured but untrusted proxy instead of ignoring it. Audit and # enable the trust switch for proxy-only deployments needing these features. # NO_PROXY is ignored even for approved private models. A selected proxy must # reach/authorize them; otherwise use a separate model process without it. # Dual-stack localhost requires both 127.0.0.1/32 and ::1/128 approval. # Invalid model-origin/CIDR configuration fails web/worker startup, naming the # offending variable without exposing its configured value. # 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" # This development-only flag affects MCP OAuth discovery, token exchange and # MCP OAuth grant refresh (including runtime grant refresh), and the MCP # connection-test path's custom OAuth transport. That test can accept a private # endpoint or another origin; passing it does not prove runtime compatibility. # Default managed HTTP/SSE/WebSocket sessions and integration OAuth refresh # remain public-only even when the flag is enabled. Configure # the final HTTPS endpoint: managed MCP rejects all changed-origin redirects, # including HTTP -> HTTPS upgrades, before forwarding credentials or bodies. # Breaking change: managed="internal" does not authorize private/loopback # HTTP MCP endpoints. See docs/mcp-outbound-security.md for migration options. # 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" # Managed MCP HTTP/SSE and integration OAuth refresh use this explicit opt-in # too. The trusted proxy must enforce public-only connect-time DNS policy. # Managed WebSocket uses a local gate chaining CONNECT to validated IPs; # the upstream must permit CONNECT on the destination port. Without this opt-in, # a configured HTTP(S)_PROXY is rejected rather than silently bypassed. # Crawlers ignore ambient proxies unless this opt-in is enabled. When enabled, # their local egress gate chains through the trusted HTTP(S) proxy using CONNECT # to validated literal IPs (including plain HTTP origins), preserving Host/SNI. # The upstream must allow CONNECT to the target ports; NO_PROXY cannot bypass it. # Browser automation continues to use direct, public-IP-pinned egress. # Public-only Requests integration clients use this opt-in too. They ignore # untrusted ambient/OS proxies and disable .netrc; explicit untrusted proxies # are rejected. Fetchers and connectors using get_trusted_proxy_url instead # reject a configured-but-untrusted proxy, rather than silently bypassing it. # Trusted proxies must enforce public-only DNS at connect time; NO_PROXY cannot # bypass this boundary. REQUESTS_CA_BUNDLE/CURL_CA_BUNDLE and SSL_CERT_FILE/DIR # remain supported without enabling ambient proxy/.netrc trust. Explicit verify # paths/False and Session.verify take precedence. OAuth stdio launchers forward # the operator trust switch, proxy URLs and CA settings together. # Admin-configured integration OAuth code exchanges require public token hosts. # Refresh protection is supplied by #3192, not by this connector-only migration; # deploy that refresh change before relying on a public-only refresh guarantee. # Persistent Requests sessions cache CA-only TLS contexts by SSL_CERT_FILE/DIR; # client-certificate contexts remain isolated. Recreate sessions after rotating # CA file contents in place; changing the configured CA path creates a new context. # 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="" # Keep Chromium sandboxing enabled by default. Only set this to true for a # deployment that deliberately accepts the risk and cannot run Chromium with # its sandbox (for example a root-running container). # XAGENT_BROWSER_ALLOW_UNSANDBOXED="false" # 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 # Under "App Home" > "Show Tabs", enable the Messages Tab. In an app # manifest this is features.app_home.messages_tab_enabled: true. This app-level # setting is required to start a DM by passing a user ID to chat.postMessage; # reconnecting the connector or changing OAuth scopes cannot enable it. # 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="" # Let the agent schedule Zoom meetings (zoom_create_meeting). This adds # meeting:write:meeting to the scopes requested when a user connects Zoom. # Every requested scope must be on the Zoom app, or Zoom may reject the whole # authorization, read-only connections included. Enable it in this order: # 1. add meeting:write:meeting to the Zoom app above; # 2. set this to true and restart. # Use the same value on every xagent process that shares the database (web and # workers): each one rewrites the stored Zoom row at startup, and a process # with the other value then marks Zoom unavailable, read tools included. # Connections made before it was enabled keep reading meetings, but must # disconnect Zoom and connect it again to create meetings. # false (default): read-only Zoom; the scope is not requested and # zoom_create_meeting is not offered. XAGENT_ZOOM_MEETING_WRITE_ENABLED=false # =========================================== # 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="" # =========================================== # Zendesk OAuth Configuration (Optional) # =========================================== # Required for the Zendesk integration in MCP (search and manage tickets, # reply or add internal notes, look up users and organizations). Each user # enters their Zendesk subdomain (the "acme" in acme.zendesk.com) before # consent; authorization, token exchange, refresh, and API calls all go to # that account. Requested scopes: read, tickets:write. # - Production: connecting accounts other than the client's own requires a # Zendesk *global* OAuth client, which Zendesk must approve # (developer.zendesk.com/documentation/marketplace/building-a-marketplace-app/set-up-a-global-oauth-client/). # Apply with exactly the scopes above. # - Development: a local OAuth client (Admin Center > Apps and integrations # > APIs > OAuth Clients) only authorizes users of the account that owns # it, which is enough to test against a Zendesk developer (d3v-) account. # Upgrade note: Zendesk no longer issues API tokens to new accounts, so the # migration for this connector deletes every API-token Zendesk connection # and users must reconnect through OAuth. Do not run it in production # before the global client is approved. Changing the client later (e.g. # from a local to the global client) also invalidates every existing # grant, so users reconnect again. An admin-created "zendesk-*" provider # row shares this account flow, and its existing grants need a reconnect. # These are read at request time: the seeded provider row leaves them blank, # so switching clients is an env change. Values entered in the admin # provider settings take precedence over these. ZENDESK_CLIENT_ID="" ZENDESK_CLIENT_SECRET="" # Redirect URI for Zendesk OAuth callback — must exactly match a redirect # URL registered on the OAuth client. # Example: http://localhost:8000/api/auth/zendesk/callback ZENDESK_REDIRECT_URI="" # Zendesk Marketplace request headers. Zendesk requires a Marketplace # integration app to send X-Zendesk-Marketplace-Name, -Organization-Id, and # -App-Id on every request to a customer's account, and checks for them # during app review. Set these only for the deployment that runs the # approved global OAuth client; a deployment with its own client leaves them # unset and sends no extra headers. Each header is sent when its variable is # set, so set the name and organization ID before submitting the app and # add the app ID once Zendesk assigns it. Values must be printable ASCII and # at most 256 characters; an invalid value is ignored with a warning. # - NAME: the integration's name; it may change over time. # - ORGANIZATION_ID: from the Organization page of the developer portal. # - APP_ID: shown on the app tile under Apps > Approved Apps. ZENDESK_MARKETPLACE_NAME="" ZENDESK_MARKETPLACE_ORGANIZATION_ID="" ZENDESK_MARKETPLACE_APP_ID="" # =========================================== # 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//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: # - If embedding API keys are configured, LanceDB will be used automatically # - LanceDB storage path: /memory_store/ # - Without embedding keys, InMemory storage will be used # - Milvus support is currently optional at provider layer and does not change defaults # =========================================== # Skills Directories Configuration # =========================================== # Comma-separated list of skill directory paths # Supports: # - Absolute paths: /path/to/skills1,/path/to/skills2 # - Relative paths: ./skills,../shared_skills # - Home directory: ~/skills,~/custom/skills # - Environment variables: $HOME/skills,${USERPROFILE}/skills # # Examples: XAGENT_EXTERNAL_SKILLS_LIBRARY_DIRS="" # XAGENT_EXTERNAL_SKILLS_LIBRARY_DIRS="/path/to/custom/skills" # XAGENT_EXTERNAL_SKILLS_LIBRARY_DIRS="~/skills,/usr/local/skills,$HOME/custom_skills" # XAGENT_EXTERNAL_SKILLS_LIBRARY_DIRS="./local_skills,../shared_skills" # =========================================== # Storage and Directory Configuration # =========================================== # Root directory for all xagent data (default: ~/.xagent) # XAGENT_STORAGE_ROOT="" # Uploads directory for user files (default: src/xagent/web/uploads) # For containerized deployments, use a persistent volume path. # The value must name one directory unambiguously: a spelling whose lexical # reading and whose symlink-resolved reading are different directories -- in # practice a ".." segment after a symlink -- is refused at startup. Sandbox # mount identity is lexical while file access resolves symlinks, so such a # value would put uploads outside the tree the sandbox mounts. Plain symlinks # are fine. The same rule applies when the root comes from XAGENT_WEB_DIR. # XAGENT_UPLOADS_DIR="" # Maximum per-file upload size enforced by the backend. # Nginx maintains a separate, larger defense-in-depth ceiling (see docker/nginx.conf). # Supports raw bytes or human-readable values like 100M, 1G, 512K. XAGENT_MAX_UPLOAD_SIZE="100M" # Format-readability validation for generated artifacts. Unsupported formats, # absent optional parsers and exhausted budgets are reported as unchecked. XAGENT_ARTIFACT_VALIDATION_MAX_BYTES=32M XAGENT_ARTIFACT_VALIDATION_TIMEOUT_SECONDS=8 # Durable file storage for user-visible uploads and registered workspace outputs. # Defaults to file://$XAGENT_STORAGE_ROOT/files for local development. # For S3-compatible storage, use a URI with bucket and optional prefix. # XAGENT_FILE_STORAGE_URI="file:///home/xagent/.xagent/files" # XAGENT_FILE_STORAGE_URI="s3://xagent-bucket/prod/files" # Optional fsspec provider options as JSON. For S3-compatible providers this can # include endpoint_url, region_name, profile, key, secret, or config_kwargs. # S3 defaults use standard Botocore retry mode with three total attempts (one # initial plus two retries) and bounded client timeouts. An explicit # config_kwargs.retries mapping replaces the default retry mapping. # XAGENT_FILE_STORAGE_OPTIONS='{"endpoint_url":"https://s3.example.com","region_name":"us-east-1"}' # Local temp/cache directory for materializing durable files when libraries need paths. # XAGENT_FILE_MATERIALIZE_DIR="/tmp/xagent-materialized" # Sync DB-registered local files to S3 durable storage during backend startup. # Only applies when XAGENT_FILE_STORAGE_URI uses s3://. Missing local files are # logged and skipped; unexpected S3 errors fail startup. # XAGENT_FILE_STORAGE_STARTUP_SYNC_ENABLED="true" # Redirect authenticated preview/download requests to short-lived durable-object # URLs when the storage backend can sign them. Keep disabled unless the signed # URL endpoint is reachable by browsers (for example public S3/MinIO or CDN). # For a media-streaming ticket's request (see XAGENT_FILE_STREAM_TICKET_TTL_SECONDS # below), this TTL governs everything after the first redirect hop, so a long # video's effective playback session is min(ticket TTL, this TTL) -- raising one # without the other doesn't extend it. # XAGENT_FILE_DELIVERY_REDIRECT_ENABLED="false" # XAGENT_FILE_DELIVERY_SIGNED_URL_TTL_SECONDS="300" # Let nginx serve local upload bytes after the backend authorizes a file_id. # This requires docker/nginx.conf's internal location and a shared read-only # uploads volume mounted into nginx. Keep disabled for direct local backend runs. # XAGENT_FILE_DELIVERY_ACCEL_REDIRECT_ENABLED="false" # XAGENT_FILE_DELIVERY_ACCEL_REDIRECT_PREFIX="/_xagent_internal_files/" # Lifetime of a short-lived, per-file media-streaming ticket (see # GET /api/files/stream-tickets/{file_id}). Media elements load this ticket # directly in a URL, so it is kept far shorter than the user's own access # token TTL. When XAGENT_FILE_DELIVERY_REDIRECT_ENABLED is also set, see # XAGENT_FILE_DELIVERY_SIGNED_URL_TTL_SECONDS above for how the two interact. # XAGENT_FILE_STREAM_TICKET_TTL_SECONDS="600" # External upload directories for knowledge base file access. # Comma-separated list of existing directory paths. Symlink aliases authorize # the physical directory they resolve to. Avoid ambiguous spellings such as # "symlink/.."; configure the intended directory directly. # Sandbox-enabled deployments must not place one of these mounts at or below # the managed /user_ subtree. Put shared data elsewhere # under the uploads root (for example /shared) or mount an ancestor. # XAGENT_EXTERNAL_UPLOAD_DIRS="/path/to/uploads1,/path/to/uploads2" # Web module directory (default: src/xagent/web) # XAGENT_WEB_DIR="" # Built frontend static export served by the backend in single-process # (pip / uvx) mode (default: $XAGENT_WEB_DIR/frontend_dist). Leave unset unless # serving a frontend build from a custom location. # XAGENT_FRONTEND_DIST_DIR="" # Optional isolated HTML runner: HTTPS origin, loopback HTTP, or "auto". # Must match NEXT_PUBLIC_XAGENT_HTML_PREVIEW_ORIGIN baked into the frontend. # This runtime value permits the runner in the packaged SPA's frame-src CSP. # The official PyPI wheel has it disabled; use a custom frontend build to enable it. # NEXT_PUBLIC_XAGENT_HTML_PREVIEW_ORIGIN="" # LanceDB database path (default: data/lancedb, relative to cwd) # For production, use an absolute path under XAGENT_STORAGE_ROOT # LANCEDB_PATH="" # Deadline in seconds for a single knowledge base collection listing scan used by # GET /api/kb/collections. Applies per scan (personal plus each team-KB owner), not # to the whole request. Raise it for very large collections; exceeding it returns 503. # XAGENT_KB_COLLECTIONS_TIMEOUT_SECONDS="30" # Maximum time in seconds to poll a Google Drive long-running export during # POST /api/kb/ingest-cloud. The final file transfer can extend the request beyond # this value, so reverse-proxy timeouts must also allow for transfer time. # XAGENT_GOOGLE_DRIVE_DOWNLOAD_TIMEOUT_SECONDS="600" # Database encryption key # Generate one with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # Configure one private key shared by web and workers before starting. # Empty values and published development keys are rejected outside development # (also in development with XAGENT_REQUIRE_SECURE_SECRETS=true). # Model/channel/OAuth/MCP/runtime values use this key. Changing it without an # offline migration can block production startup for durable credentials. # Follow docs/tool-credential-key-upgrade.md before rotating; reconnectable # OAuth accounts and transient flows/runs warn instead and remain unusable # until reauthorized/restarted. Never overwrite original unreadable ciphertext. ENCRYPTION_KEY="" # Mandatory outside development, independently of JWT signing. Preserve the # existing value across upgrades: changing it makes stored tool credentials # unreadable. SECRET_KEY remains a legacy alias; use a random 32+ character key. XAGENT_SECRET_ENCRYPTION_KEY="" # Offline migration only, never used as a runtime decryption fallback. See # docs/tool-credential-key-upgrade.md before upgrading old/short keys. # XAGENT_TOOL_CREDENTIALS_MIGRATION_KEY="" # Offline ENCRYPTION_KEY re-encryption only; never a runtime fallback: # XAGENT_ENCRYPTION_MIGRATION_KEY="" # =========================================== # Tool Output Configuration # =========================================== # Maximum length per string in tool output (default: 51200, ~50KB) # This limits individual string values, not total output size. # It is also the threshold above which a dict tool result, or a value inside # one, is stored as a file in the task workspace instead of being truncated. # Storing happens only when the tool set includes read_tool_result bound to a # task workspace; otherwise over-long strings are truncated as before. # Stored files go to output/tool-results/ in the task workspace, at most # SPILL_MAX_FILES_PER_RUN files per tool set # (src/xagent/core/tools/tool_result_spill.py). The model reads them back with # read_tool_result, at most SPILL_READ_MAX_CHARS (12000) characters per call, # or this value per call when it is lower. # See docs/tool-result-storage.md. # XAGENT_TOOL_MAX_OUTPUT_LENGTH="51200" # Maximum number of fields/items in dict/list (default: 1000) # This limits collection cardinality to prevent excessive output # XAGENT_TOOL_MAX_FIELD_COUNT="1000" # Maximum recursion depth for nested structures (default: 20) # This prevents excessively deep nesting in tool output # XAGENT_TOOL_MAX_RECURSION_DEPTH="20" # =========================================== # External Database Connections (for SQL Query Tool) # =========================================== # SQL tool supports PostgreSQL, MySQL, and SQLite # Connection format: XAGENT_EXTERNAL_DB_= # # Install required database drivers: # - PostgreSQL: pip install psycopg2-binary (or psycopg2) # - MySQL: pip install pymysql (or mysqlclient) # - SQLite: built-in (no installation needed) # # Format of the url is the same as used by SQLAlchemy, async drivers are not supported currently. # Examples: # XAGENT_EXTERNAL_DB_ANALYTICS="postgresql://user:password@localhost:5432/analytics" # XAGENT_EXTERNAL_DB_PROD="mysql+pymysql://user:password@localhost:3306/production" # XAGENT_EXTERNAL_DB_LOCAL="sqlite:///path/to/database.db" # =========================================== # Sandbox Configuration # =========================================== # Shared sandbox execution isolates each process using a stable, unique # XAGENT_SANDBOX_WORKER_ID. Set a distinct ID for every concurrent process, # including each uvicorn worker; reuse it only after the old process stops. # Local execution keeps the single-process sandbox ownership model. # See docs/shared-worker-deployment.md for upgrades and replica configuration. # # Enable/disable sandbox execution environment (default: false) # SANDBOX_ENABLED="false" # Sandbox implementation type (default: docker) # Available options: docker, boxlite # SANDBOX_IMPLEMENTATION="docker" # Required when sandbox execution uses the Docker implementation. The Docker # Compose sibling overlay sets this to COMPOSE_PROJECT_NAME automatically. # Non-Compose deployments (pip/systemd) must choose one stable, unique value # per deployment. # XAGENT_SANDBOX_NAMESPACE="my-deployment" # Recommended Docker deployment topology: # - Prefer a dedicated Docker engine for sandbox workloads, for example a separate VM, # a dedicated Docker host, or a dedicated DinD/containerized Docker daemon used only by Xagent. # - Avoid sharing the host's primary Docker daemon with sandbox execution if possible. # # Security warning: # - Do NOT mount the host Docker socket (for example /var/run/docker.sock) directly into the # Xagent backend when it may execute untrusted sandbox code. # - Access to that socket is effectively root-equivalent on the Docker host: an attacker could # start privileged containers, mount host filesystems, read secrets, or escape the sandbox # and gain control of the host. # # Suggested hardening measure: # - Put a restricted proxy such as Tecnativa/docker-socket-proxy in front of the Docker # daemon. # # When using SANDBOX_IMPLEMENTATION="docker", set DOCKER_HOST if your Docker-compatible # runtime endpoint is not the default (for example Podman socket/service) # DOCKER_HOST="" # Docker sibling deployments resolve sandbox code mounts on the Docker host, # not inside the Xagent backend container. Set this to the host-side project # checkout root when it differs from the backend container path. # XAGENT_SANDBOX_HOST_PROJECT_ROOT="" # Docker sibling deployments also resolve workspace/storage mounts on the Docker # host. Set this to the host-side Xagent storage root when manual deployments # bind a different host path into the backend as XAGENT_STORAGE_ROOT. # The provided Compose overlay accepts XAGENT_HOST_STORAGE_ROOT and passes it # through as XAGENT_SANDBOX_HOST_STORAGE_ROOT automatically. # When set, this must be an absolute Docker-host path. Relative paths and "~" # are rejected rather than being interpreted in the backend container. # XAGENT_SANDBOX_HOST_STORAGE_ROOT="" # Set automatically for the sandbox tool runner process; never set it yourself. # It tells workspace file registration that this process has no database or # object storage credentials, so the host process registers sandbox outputs # instead. Setting it on a host process makes that process mint throwaway ids # for every file it registers, so nothing it produces is ever persisted. Read # once at process start, so changing it at runtime has no effect. # XAGENT_SANDBOX_TOOL_RUNNER="" # Wall-clock limit in seconds for one execute_python_code call, sandboxed or # not. On expiry the Python process group is killed (POSIX hosts and the # sandbox) and the agent gets a timeout error instead of waiting forever # (default: 1200). # XAGENT_PYTHON_EXECUTOR_TIMEOUT_SECONDS="1200" # Boxlite home directory (default: ~/.boxlite) # Takes effect only when using the boxlite implementation # BOXLITE_HOME_DIR="" # Sandbox container image (default: xprobe/xagent-sandbox:latest) # We should pin the version at release (`latest` may lead to caching problems) # SANDBOX_IMAGE="" # Sandbox CPU core limit (default: 1) # SANDBOX_CPUS="" # Sandbox memory limit in MB (default: 512) # SANDBOX_MEMORY="" # Max concurrent worker sandboxes per user/task lifecycle. This only affects # sandboxed tools that explicitly opt into concurrency-safe execution. # Invalid/non-positive values fall back to the default. # XAGENT_SANDBOX_MAX_CONCURRENCY="3" # =========================================== # Sandbox Idle Reclamation # =========================================== # Idle TTL in seconds after which sandbox containers with no active tasks are # deleted by a background sweep (default: unset — containers live forever, # matching previous behavior). # # Reclamation deletes the container. Workspace data lives on bind mounts and # survives; the sandbox is transparently recreated on next use (cold-start # latency only). Anything written OUTSIDE the mounted workspace/uploads paths # is lost: lazily installed pip packages (reinstalled on demand), files under # /tmp or $HOME inside the container. Containers found at backend startup # with no recorded activity are treated as idle since startup and reclaimed # one TTL later. # XAGENT_SANDBOX_IDLE_TTL="86400" # Interval in seconds between idle sweep runs (default: 60). Only meaningful # when XAGENT_SANDBOX_IDLE_TTL is set. # XAGENT_SANDBOX_SWEEP_INTERVAL="60" # =========================================== # Sandbox Capacity Limit # =========================================== # Maximum number of concurrently existing sandbox containers (default: unset # — no cap, matching previous behavior). The cap counts all managed # containers including per-lifecycle workers; the transient warmup container # is excluded. # # When a new sandbox is requested at the cap, the least-recently-used idle # sandbox (no active tasks) is deleted to make room. If nothing is evictable # (all sandboxes busy), a task's primary sandbox request fails at task # admission with a clear capacity error by default instead of silently # running on the host. A worker sandbox for a concurrency-safe tool that # cannot fit under the cap (a task's own primary occupies a slot and is # never evicted to make room for its workers) degrades to running on the # task's primary sandbox with a warning log — reduced isolation, but the # tool keeps working mid-task. # # Size the cap for full worker isolation: allow roughly expected concurrent # primaries x (1 + XAGENT_SANDBOX_MAX_CONCURRENCY) containers. # # The ref-count and cap apply separately to each execution process and its # isolated sandbox resources, not to the deployment total. # XAGENT_SANDBOX_MAX_CONTAINERS="20" # Allow tasks to fall back to local (host) execution when the sandbox cap is # reached and nothing is evictable (default: false — reject the task). # Enable only if you prefer availability over strict sandboxing; sandbox # service unavailability keeps its local fallback regardless of this flag. # XAGENT_SANDBOX_ALLOW_LOCAL_FALLBACK_ON_CAPACITY="false" # =========================================== # Sandbox Volume Mount Configuration # =========================================== # Mount host directories into sandbox (Docker-style format) # Format: SANDBOX_VOLUMES="src:dst:mode;src:dst:mode" # - src: Path on host machine (supports ~ expansion) # - dst: Path inside sandbox container # - mode: 'ro' (read-only) or 'rw' (read-write), default is 'ro' when omitted # - Multiple volumes are separated by semicolon (;) # - In Docker sibling mode, src is interpreted by the Docker host daemon. # Use absolute host paths; relative paths and ~ are rejected. # - dst must not equal or fall below the managed # /user_ subtree. A nested bind would shadow the # task workspace. Use a destination elsewhere or mount an ancestor. # # Examples: # Mount Python venv site-packages (read-only recommended) # SANDBOX_VOLUMES="/home/user/my-python-site:/usr/local/lib/python3.14/site-packages:ro" # # Mount node_modules # SANDBOX_VOLUMES="/home/user/my-node_modules:/usr/local/lib/node_modules" # # Mount multiple directories # SANDBOX_VOLUMES="/home/user/my-python-site:/usr/local/lib/python3.14/site-packages;/home/user/my-python-app:/opt/my-python-app" # =========================================== # Sandbox Environment Variables # =========================================== # Additional environment variables to inject into sandbox # Format: SANDBOX_ENV="KEY1=value1;KEY2=value2" # - Multiple variables are separated by semicolon (;) # - Values should not contain semicolon # # Example: Set PYTHONPATH to include mounted site-packages # SANDBOX_ENV="PYTHONPATH=/home/user/my-python-app:/opt/my-python-app" # =========================================== # Interaction Rollout Configuration # =========================================== # Default-off gate controlling whether tasks may publish native interaction # rows. One of: legacy, read, native. Stays "legacy" (no native rows # produced) unless explicitly switched. # XAGENT_INTERACTION_PROTOCOL_MODE="legacy" # Comma-separated list of task sources allowed to publish native rows when # mode is "native" (ignored otherwise). Each entry must be one of: internal, # sdk, a2a, trigger, widget, shared_link, channel. Native mode requires at # least one entry here. Native publication also requires shared task # execution; a process set to native that is not running it logs a startup # warning and never publishes. # XAGENT_INTERACTION_NATIVE_SOURCES="" # Shared execution requires Redis, a private common ENCRYPTION_KEY, and shared # file storage. It is selected automatically when XAGENT_WORKER_COUNT is set or # the role is web/worker; otherwise an unset value uses local combined mode. # Set this explicitly to pin either behavior. Apply database migrations before # starting standalone workers. # XAGENT_SHARED_TASK_EXECUTION_ENABLED=true # combined serves HTTP/WS and executes tasks; web only accepts tasks; # worker uses python -m xagent.web.worker and does not serve HTTP/WS. XAGENT_TASK_EXECUTION_ROLE=combined # Optional: xagent-web / python -m xagent.web starts one web process plus N # standalone Agent workers. Requires shared execution + combined; no --reload. # Published backend images built from the current source default to 2; an # unconfigured wheel remains single-process. This is a process count, not a task # concurrency limit. With sandboxing, worker IDs are derived from # XAGENT_SANDBOX_WORKER_ID as -worker-1 through -worker-N. # XAGENT_WORKER_COUNT=4 # Enable on exactly one designated web/combined host for Telegram/Feishu/Slack. # Worker replicas never start bot connections. XAGENT_CHANNEL_INGRESS_ENABLED=false # Redis Pub/Sub ignores logical DB numbers; namespace each deployment separately. XAGENT_TASK_EVENT_CHANNEL_PREFIX=xagent:task-events:v1 # Runtime credentials survive pause/wait for this many seconds from acceptance. # Expired credentials must be supplied again; resuming does not extend this TTL. XAGENT_TASK_RUNTIME_SECRETS_TTL_SECONDS=86400 # Required when shared execution uses Docker/BoxLite. Stable and unique per # concurrently running execution process; override separately on every replica. # XAGENT_SANDBOX_WORKER_ID=worker-1 # Deployment and upgrade procedure: docs/shared-worker-deployment.md # --------------------------------------------------------------------------- # Conversation data retention (#2557). Disabled: with no period set below, # nothing is ever expired, which is the shipping default until the retention # policy is decided. # # The purge job reads these. Setting a period turns it on; see the retention # section of docs/deployment.md before you do, and start with the dry run. # # Every value here is read once, at process start: changing any of them takes # a restart, including the kill switch. Nothing re-reads this file, and no # code assigns these names at run time. # --------------------------------------------------------------------------- # Days of conversation retention, measured from the task's last conversational # activity -- not from any execution bookkeeping, and falling back to the # task's creation time only when no activity was ever recorded. Unset or 0 # disables conversation expiry; a value that cannot be read as a whole number # of days also disables it, rather than falling back to some default. Traces # can still expire under their own period below. # XAGENT_CONVERSATION_RETENTION_DAYS=365 # Days of execution-trace retention. Unset or 0 means "same as the conversation # period". Set it shorter to expire traces (the bulk of stored bytes) while # keeping the conversation; set it alone to keep conversations indefinitely. # A value that cannot be read as a whole number of days disables trace expiry # rather than falling back to the conversation period above. # XAGENT_TRACE_RETENTION_DAYS=90 # Kill switch: set false to stop expiry without editing the periods above. # Takes effect on restart, like every other value here. # # The two switches below accept 1/true/yes/on/y and 0/false/no/off/n, which is # wider than the other XAGENT_* flags in this file -- those read anything they # do not recognise as false. These two warn instead, and resolve what they # cannot read to whichever side deletes nothing: false here, true for the # dry-run flag. An empty value counts as unset, not as a mistake, so # XAGENT_RETENTION_ENABLED=${SOMETHING_UNSET} behaves as if the line were absent. # XAGENT_RETENTION_ENABLED=true # Report what would be expired and write nothing. Performs no deletes. A value # the parser does not recognise resolves to true: this is the setting to reach # for before a first real run, so a typo in it must not become a deletion. An # empty value, though, counts as unset -- it says nothing, so the default # applies. # XAGENT_RETENTION_DRY_RUN=false # Tasks considered per batch. # XAGENT_RETENTION_BATCH_SIZE=100 # Seconds between sweeps once the eligible backlog is drained. # XAGENT_RETENTION_SWEEP_INTERVAL_SECONDS=86400 # Seconds to pause between batches while a backlog remains. This is the rate # limit: raise it if bulk deletion pressures autovacuum or replication. # XAGENT_RETENTION_BATCH_PAUSE_SECONDS=5 ## Execution token budget (reported input + output, per execution turn). ## Leave token limits unset to preserve unlimited execution. Personal settings ## may override the default but cannot exceed the administrator's maximum. ## These are initial defaults: once administrator settings are saved in the UI, ## the database settings replace all three env values, including the maximum. ## Subsequent env changes have no effect; clearing the saved maximum removes it. # XAGENT_EXECUTION_BUDGET_DEFAULT_TOKENS=200000 # XAGENT_EXECUTION_BUDGET_MAX_TOKENS=500000 # XAGENT_EXECUTION_BUDGET_SOFT_PERCENT=80 # Maximum remote provider-result download sizes: positive byte counts or size # suffixes (64M, 1G, 1.5M). Empty values use defaults; bare fractions are invalid. # These do not restrict configured model API endpoints or enable private downloads. # XAGENT_IMAGE_DOWNLOAD_MAX_BYTES=67108864 # XAGENT_VIDEO_DOWNLOAD_MAX_BYTES=536870912 # Maximum remote JSON input size for speech synthesis batches (positive integer bytes). # XAGENT_REMOTE_JSON_DOWNLOAD_MAX_BYTES=10485760