a26803a1bc
The resumable-SSE run-stream registry (PR #386/#387) ships behind the server-side AI_CHAT_RESUMABLE_STREAM env flag, OFF by default: with the flag off attach always answers 204 and reopened tabs of an active run fall back to degraded history polling. Document the flag, its default, its relation to the per-workspace autonomousRuns setting, and the single-instance constraint in .env.example next to the autonomous-runs section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
295 lines
15 KiB
Bash
295 lines
15 KiB
Bash
# your domain, e.g https://example.com
|
|
APP_URL=http://localhost:3000
|
|
PORT=3000
|
|
|
|
# --- Security / reverse proxy ---
|
|
# The app derives the client IP (req.ip) from the `X-Forwarded-For` header via
|
|
# Fastify `trustProxy`. That header is client-forgeable, so XFF is trusted only
|
|
# from proxies on the configured trusted networks. Deploy this app behind a
|
|
# trusted reverse proxy that SETS/OVERWRITES (not appends) `X-Forwarded-For`
|
|
# with the real client IP. If XFF is trusted from an untrusted source, any
|
|
# per-IP throttling — including the /mcp Basic brute-force limiter — can be
|
|
# bypassed by an attacker who simply spoofs `X-Forwarded-For` to rotate IPs.
|
|
# (The /mcp limiter keeps a global per-email key as an IP-independent backstop,
|
|
# but the per-IP and per-IP+email keys rely on a trustworthy X-Forwarded-For.)
|
|
#
|
|
# TRUST_PROXY controls which proxies are trusted to set X-Forwarded-For.
|
|
# Default (unset/empty): `loopback, linklocal, uniquelocal` — XFF is trusted
|
|
# ONLY from private/loopback proxies, so a public-IP client cannot spoof req.ip.
|
|
# This is the safe default for the common case where the reverse proxy runs on
|
|
# loopback or a private network; req.ip still resolves to the real client.
|
|
# WARNING: this changed the previous default of trust-all. If your reverse proxy
|
|
# sits on a PUBLIC IP, the default will NOT trust its XFF and req.ip will be the
|
|
# proxy's IP — set TRUST_PROXY accordingly. Accepted values:
|
|
# - true restore trust-all (ONLY safe if a trusted proxy ALWAYS overwrites
|
|
# X-Forwarded-For; otherwise clients can spoof their IP)
|
|
# - false never trust X-Forwarded-For (req.ip is the socket peer)
|
|
# - <int> number of trusted proxy hops in front of the app
|
|
# - <list> comma-separated CIDR/IP list of trusted proxies, e.g.
|
|
# `127.0.0.1, 10.0.0.0/8`
|
|
# TRUST_PROXY=
|
|
|
|
# APP_SECRET has a DUAL role: it signs JWTs AND derives the AES-256-GCM key that
|
|
# encrypts stored AI-provider credentials (API keys) at rest. CONSEQUENCE: if you
|
|
# change APP_SECRET after setup, every stored AI API key becomes undecryptable —
|
|
# you must re-enter them in AI settings — and all existing sessions/JWTs are
|
|
# invalidated. Choose it ONCE, keep it stable, and back it up alongside your DB.
|
|
# minimum of 32 characters. Generate one with: openssl rand -hex 32
|
|
APP_SECRET=REPLACE_WITH_LONG_SECRET
|
|
|
|
JWT_TOKEN_EXPIRES_IN=30d
|
|
|
|
DATABASE_URL="postgresql://postgres:password@localhost:5432/docmost?schema=public"
|
|
REDIS_URL=redis://127.0.0.1:6379
|
|
|
|
# options: local | s3 | azure
|
|
STORAGE_DRIVER=local
|
|
|
|
# S3 driver config
|
|
AWS_S3_ACCESS_KEY_ID=
|
|
AWS_S3_SECRET_ACCESS_KEY=
|
|
AWS_S3_REGION=
|
|
AWS_S3_BUCKET=
|
|
AWS_S3_ENDPOINT=
|
|
AWS_S3_FORCE_PATH_STYLE=
|
|
|
|
# Azure Blob Storage driver config
|
|
AZURE_STORAGE_ACCOUNT_NAME=
|
|
AZURE_STORAGE_ACCOUNT_KEY=
|
|
AZURE_STORAGE_CONTAINER=
|
|
|
|
# default: 50mb
|
|
FILE_UPLOAD_SIZE_LIMIT=
|
|
|
|
# options: smtp | postmark
|
|
MAIL_DRIVER=smtp
|
|
MAIL_FROM_ADDRESS=hello@example.com
|
|
MAIL_FROM_NAME=Docmost
|
|
|
|
# SMTP driver config
|
|
SMTP_HOST=127.0.0.1
|
|
SMTP_PORT=587
|
|
SMTP_USERNAME=
|
|
SMTP_PASSWORD=
|
|
SMTP_SECURE=false
|
|
SMTP_IGNORETLS=false
|
|
|
|
# Postmark driver config
|
|
POSTMARK_TOKEN=
|
|
|
|
# for custom drawio server
|
|
DRAWIO_URL=
|
|
|
|
# Gotenberg URL for server-side PDF export
|
|
GOTENBERG_URL=
|
|
|
|
DISABLE_TELEMETRY=false
|
|
|
|
# Allow other sites to embed Docmost in an iframe.
|
|
IFRAME_EMBED_ALLOWED=false
|
|
|
|
# Only used when IFRAME_EMBED_ALLOWED=true. When empty, any origin is allowed.
|
|
# Example: https://intranet.example.com,https://portal.example.com
|
|
IFRAME_ALLOWED_ORIGINS=
|
|
|
|
# Enable debug logging in production (default: false)
|
|
DEBUG_MODE=false
|
|
|
|
# Log database queries
|
|
DEBUG_DB=false
|
|
|
|
# Log http requests
|
|
LOG_HTTP=false
|
|
|
|
# MCP server (community): the embedded /mcp endpoint authenticates PER USER.
|
|
# An MCP client authenticates with one of:
|
|
# - HTTP Basic: `Authorization: Basic base64(email:password)` — the user's own
|
|
# Docmost login/password. The server validates the credentials and the MCP
|
|
# session then acts under that user's permissions (edits attributed to them).
|
|
# - Bearer access JWT: `Authorization: Bearer <access-jwt>` (the user's
|
|
# `authToken` cookie value). Validated as an ACCESS token.
|
|
#
|
|
# OPTIONAL service-account fallback. When a request carries NEITHER Basic NOR
|
|
# Bearer credentials and these are set, the MCP session falls back to this
|
|
# shared service account (back-compat; useful for CI/scripts). Leave BLANK to
|
|
# require per-user credentials.
|
|
MCP_DOCMOST_EMAIL=
|
|
MCP_DOCMOST_PASSWORD=
|
|
# MCP_DOCMOST_API_URL=http://127.0.0.1:3000/api
|
|
# Optional shared guard for the /mcp endpoint. When set, every /mcp request must
|
|
# carry a matching `X-MCP-Token` header (separate from `Authorization`, which now
|
|
# carries the per-user credentials). When unset, /mcp relies on the per-user
|
|
# credentials above plus the workspace MCP toggle and network isolation (do not
|
|
# expose the port publicly).
|
|
# MCP_TOKEN=
|
|
# MCP_SESSION_IDLE_MS=1800000
|
|
#
|
|
# BLOB SANDBOX (stash_page). An in-RAM, process-local store that hands large page
|
|
# content + images to an external consumer WITHOUT bloating the model context or
|
|
# requiring Docmost auth. The stash_page tool serializes a page, mirrors its
|
|
# internal images into the store, and returns ONLY a short anonymous URL; the
|
|
# consumer fetches blobs via `GET /api/sb/<uuid>` (no token — the capability is
|
|
# the unguessable UUID + short TTL + TLS). Blobs are RAM-only and cleared on
|
|
# restart. ETag = the blob's sha256 (integrity check).
|
|
# SANDBOX_PUBLIC_URL is the base used to build those URLs; it MUST be reachable
|
|
# by the consumer (do NOT use a loopback address if the consumer is remote).
|
|
# Defaults to APP_URL when unset.
|
|
# NOTE: the store is process-local — blobs live only on the instance that
|
|
# created them. Behind a multi-replica load balancer WITHOUT sticky sessions a
|
|
# consumer may hit a different instance and get a 404 (indistinguishable from an
|
|
# expired blob). Single-host deployments are unaffected.
|
|
# SANDBOX_PUBLIC_URL=https://docs.example.com
|
|
# SANDBOX_TTL_MS=3600000
|
|
# SANDBOX_MAX_BYTES=8388608
|
|
# SANDBOX_MAX_IMAGE_BYTES=20971520
|
|
# SANDBOX_MAX_TOTAL_BYTES=134217728
|
|
#
|
|
# AI-AGENT ATTRIBUTION (comments/pages written via MCP are badged as "AI"):
|
|
# attribution is driven by a per-user `is_agent` flag on the users row. There is
|
|
# NO admin UI/API for it — set it out-of-band with SQL. Use a DEDICATED service
|
|
# account for the MCP fallback above and flag ONLY that account, e.g.:
|
|
# UPDATE users SET is_agent = true WHERE email = 'mcp-bot@your-domain';
|
|
# NEVER set is_agent on a human or shared account — every action by that account
|
|
# (including normal human edits) would then be mis-attributed as AI.
|
|
|
|
# Agent-roles catalog source: an http(s):// base URL to the catalog's raw files
|
|
# (the server appends /index.yaml and /bundles/<id>/<lang>.yaml). This value is
|
|
# baked into the Docker image at build time per branch (see the Dockerfile ARG
|
|
# AI_AGENT_ROLES_CATALOG_URL and the CI build-args). Set it here only to point a
|
|
# local/non-Docker run at a catalog; if unset, the "import role from catalog"
|
|
# admin feature is unavailable. Local-filesystem sources are no longer supported.
|
|
# AI_AGENT_ROLES_CATALOG_URL=
|
|
|
|
# Per-embedding-call timeout in milliseconds for the RAG indexer.
|
|
# A slow/hung embeddings endpoint fails after this and the batch continues.
|
|
# AI_EMBEDDING_TIMEOUT_MS=120000
|
|
|
|
# Silence timeout (ms) for streaming chat/agent AI calls AND external-MCP traffic.
|
|
# Bounds time-to-first-byte and the gap BETWEEN chunks (NOT the total turn length),
|
|
# so an arbitrarily long turn that keeps streaming is never cut. Finite so a hung
|
|
# provider is eventually broken instead of leaking forever. Default 900000 (15 min).
|
|
# AI_STREAM_TIMEOUT_MS=900000
|
|
|
|
# Keep-alive recycle window (ms) for streaming chat/agent AI + external-MCP calls.
|
|
# A pooled connection idle longer than this is closed instead of reused, so a
|
|
# NAT / egress firewall / reverse proxy that silently drops idle connections
|
|
# cannot poison a reused socket into a PRE-RESPONSE `read ECONNRESET`. Kept under
|
|
# common ~5s upstream/middlebox idle cutoffs so undici recycles the socket before
|
|
# the network kills it (fewer resets), while still reusing within a burst of
|
|
# back-to-back calls. Lower it further if your egress drops idle connections even
|
|
# faster. Default 4000 (4 s).
|
|
# AI_STREAM_KEEPALIVE_MS=4000
|
|
|
|
# Number of PRE-RESPONSE connection retries for streaming chat/agent AI calls: a
|
|
# reset/timeout BEFORE any response byte (e.g. `read ECONNRESET` on a stale pooled
|
|
# socket) is retried on a fresh connection with jittered exponential backoff.
|
|
# Total attempts = value + 1, so the default 4 gives 5 attempts — headroom to
|
|
# absorb a short BURST of upstream resets without exhausting the budget. Safe to
|
|
# retry: a started stream is never replayed, only a connect that never responded.
|
|
# 0 disables the retry. Default 4.
|
|
# AI_STREAM_PRE_RESPONSE_RETRIES=4
|
|
|
|
# Silence timeout (ms) for EXTERNAL-MCP transport ONLY (not the chat provider).
|
|
# Tighter than AI_STREAM_TIMEOUT_MS so a byte-silent/hung MCP server is broken in
|
|
# ~5 min instead of 15. Note it also cuts a legitimately long but byte-silent
|
|
# single tool call (a slow crawl that emits nothing until done) and an SSE
|
|
# transport idling >5 min BETWEEN tool calls. Default 300000 (5 min).
|
|
# AI_MCP_STREAM_TIMEOUT_MS=300000
|
|
|
|
# Total wall-clock cap (ms) for ONE external MCP tool call (app-level, not
|
|
# transport). Aborts a tool that keeps the socket warm (SSE heartbeats / trickle)
|
|
# but never returns a result — which the silence timeout above never breaks.
|
|
# Default 900000 (15 min).
|
|
# AI_MCP_CALL_TIMEOUT_MS=900000
|
|
|
|
# Deferred tool loading for the in-app AI chat (#332). Default ON: the agent sees
|
|
# a compact <tool_catalog> and only CORE tools + a loadTools meta-tool are active
|
|
# each step; deferred tools (the fat/rare ones + all external MCP tools) load on
|
|
# demand. Set AI_CHAT_DEFERRED_TOOLS=false to restore the old "all tools always
|
|
# active" behavior.
|
|
# AI_CHAT_DEFERRED_TOOLS=true
|
|
|
|
# --- Autonomous / detached agent runs (settings.ai.autonomousRuns) ---
|
|
# Opt-in per workspace (AI settings; off by default). When on, a chat turn becomes
|
|
# a server-side RUN that survives a browser disconnect — only an explicit Stop ends
|
|
# it, and a client reconnects/live-follows the run.
|
|
#
|
|
# DEPLOY CONSTRAINT — SINGLE-INSTANCE ONLY in phase 1: Stop and the in-process
|
|
# AbortController that backs it are process-local, so a Stop only aborts a run
|
|
# executing on the SAME replica that owns it (cross-instance pub/sub stop is phase
|
|
# 2 and not yet reliable). Do NOT enable autonomousRuns on a horizontally-scaled
|
|
# deployment (multiple replicas behind a load balancer, or Docmost cloud
|
|
# CLOUD=true) — run a single instance instead. The server logs a startup WARNING
|
|
# when it detects a multi-instance deployment (CLOUD=true) so the constraint is
|
|
# visible, and a startup sweep settles any run left dangling by a restart.
|
|
#
|
|
# Resumable run streams (#184 phase 1.5, #381). With the flag ON, an active
|
|
# durable run tees its SSE frames into an in-memory registry, and a
|
|
# reloaded/second tab attaches via GET /ai-chat/runs/:chatId/stream to follow the
|
|
# run LIVE (replay of the buffered frames + the live tail). With the flag OFF
|
|
# (default) the registry is never populated and attach always answers 204, so a
|
|
# reopened tab of an active run silently falls back to degraded 2.5s history
|
|
# polling — every wire path stays byte-for-byte identical to a build without the
|
|
# feature. Staged-rollout switch: only meaningful when autonomousRuns (above) is
|
|
# enabled for a workspace, and the same single-instance constraint applies (the
|
|
# registry is process-local).
|
|
# AI_CHAT_RESUMABLE_STREAM=false
|
|
|
|
# --- Anonymous public-share AI assistant ---
|
|
# Opt-in per workspace (AI settings -> "public share assistant"; off by default).
|
|
# When enabled, anonymous visitors of a published share can ask an AI about that
|
|
# share at POST /api/shares/ai/stream. The assistant is read-only and hard-scoped
|
|
# to the single share tree, but every call spends real tokens on the workspace
|
|
# owner's configured AI provider.
|
|
#
|
|
# DEPLOYMENT REQUIREMENT: the per-IP rate limit on this endpoint is only
|
|
# effective behind a trusted reverse proxy that OVERWRITES (not appends)
|
|
# X-Forwarded-For with the real client IP. The app runs with trustProxy, so
|
|
# without such a proxy an attacker can rotate X-Forwarded-For to evade the
|
|
# per-IP limit. Put this endpoint (and the app) behind a proxy you control that
|
|
# sets X-Forwarded-For to the real client IP.
|
|
#
|
|
# Backstop: a cluster-wide, sliding-window cap per workspace (IP-independent,
|
|
# keyed by the server-resolved workspace id) bounds the owner's bill even if the
|
|
# per-IP limit is fully evaded. It is a COST backstop, not an access control, and
|
|
# FAILS CLOSED if Redis is unavailable (an optional assistant briefly going
|
|
# offline is safer than an unbounded bill). Override the hourly cap below
|
|
# (default: 100 calls per workspace per rolling hour).
|
|
# SHARE_AI_WORKSPACE_MAX_PER_HOUR=100
|
|
#
|
|
# Per-request output-token ceiling for the anonymous assistant (default: 512).
|
|
# Worst-case output per accepted call = agent steps (5) × this value.
|
|
# SHARE_AI_MAX_OUTPUT_TOKENS=512
|
|
#
|
|
# Second cost backstop: a cluster-wide per-workspace rolling-DAY token budget
|
|
# (input re-sent per step + output, summed across every accepted turn). The
|
|
# hourly request cap above bounds how MANY calls run, not how expensive each is,
|
|
# so this caps the owner's actual provider bill directly. Like the request cap it
|
|
# FAILS CLOSED if Redis is unavailable (default: 1,000,000 tokens per workspace
|
|
# per rolling day).
|
|
# SHARE_AI_WORKSPACE_TOKEN_BUDGET_PER_DAY=1000000
|
|
|
|
# --- Observability / perf metrics (#355) ---
|
|
#
|
|
# Two INDEPENDENT toggles, both OFF by default:
|
|
#
|
|
# 1) METRICS_PORT — the server-side Prometheus scrape endpoint.
|
|
# UNSET (default) => the whole prom subsystem is OFF: no registry, no
|
|
# collectors, and NOTHING is exposed on the main app port. There is NO
|
|
# default port — leaving it blank disables it. When set to a port (e.g.
|
|
# 9464), a SEPARATE bare node:http listener serves GET /metrics on that port
|
|
# only (never on the main :3000 app listener), for a scraper such as
|
|
# VictoriaMetrics/Prometheus reaching it as <host>:<port>/metrics.
|
|
# METRICS_PORT=9464
|
|
#
|
|
# 2) CLIENT_TELEMETRY_ENABLED — the public client perf-telemetry sink.
|
|
# OFF by default. When true, the unauthenticated POST /api/telemetry/vitals
|
|
# endpoint is registered and browsers collect + send web-vitals / editor
|
|
# metrics into the `client_metrics` table (read directly by Grafana, separate
|
|
# from METRICS_PORT). Leave OFF unless you actually consume this data: the
|
|
# endpoint is public and the table has NO app-side retention, so enabling it
|
|
# requires an EXTERNAL pruner to bound `client_metrics` growth (the deployed
|
|
# infra prunes rows >90d via a maintenance container). When off, the endpoint
|
|
# does not exist and the client installs no observers.
|
|
# CLIENT_TELEMETRY_ENABLED=false
|