M8B Slack Bot

Configuration reference

Every setting is an environment variable read from the .env file in the installation directory (the Compose env_file); start from the installation example. Variables marked required stop the bot at startup when missing; malformed values (unknown AI_PROVIDER, a web search provider without its URL or key, bad Slack user IDs, malformed URLs) are always fatal, one line per problem.

Which features exist on which backend is in BACKENDS.md.

Slack

Variable Default Description
SLACK_BOT_TOKEN required Bot User OAuth Token (xoxb-...) from the installed app
SLACK_APP_TOKEN required App-level token (xapp-...) with connections:write for Socket Mode
SLACK_API_URL https://slack.com/api Alternate Slack API base (GovSlack, proxies)

The app is created from manifest.json (Agent messaging experience, Socket Mode, no inbound ports). The team:read bot scope lets the bot read the workspace name for its system prompt; it is optional: without it the bot falls back to the team name from auth.test and logs a warning.

Switching an existing Slack app from assistant_view to agent_view is irreversible. Update and test the application code before applying manifest.json to an existing installation. Users need to hard-refresh Slack after the manifest update. Slack CLI 4.4.0 or newer is required to apply an Agent View manifest.

AI backend selection

Variable Default Description
AI_PROVIDER openai openai, ollama, vllm or openai-compatible (see BACKENDS.md)
M8B_START_DEGRADED false Start even when the AI backend health check fails (the bot then answers every message with an error until the backend is back)
AI_MAX_AGENT_ITERATIONS 15 Cap on model → tools → model loops per Slack message. The last permitted iteration forces a text answer

At startup the bot logs the active backend and runs a health check: reachability, model presence and one minimal streaming /v1/responses request, so a backend that lists models but cannot answer is caught before the bot goes online. Function-tool support is not probed at startup; the doctor script does that. A failed check is fatal (exit code 1) unless M8B_START_DEGRADED=true.

AI provider: ollama
AI model: qwen3.8:27b
AI endpoint: http://ollama-host:11434/v1
AI backend health check passed (model "qwen3.8:27b" available)

OpenAI mode (AI_PROVIDER=openai)

Variable Default Description
OPENAI_API_KEY required API key with access to gpt-5.6-sol and the Responses API hosted tools
OPENAI_VECTOR_STORE_IDS unset Comma-separated vector store IDs for the hosted knowledge base
OPENAI_VECTOR_STORE_ID unset Single-store alternative to the above
OPENAI_CODE_CONTAINER_ID unset Reuse a Code Interpreter container (cc_...) instead of an automatic one

The model configuration lives in ai/config/system-prompt.js: gpt-5.6-sol, reasoning effort medium with automatic summaries, low verbosity, up to 8,000 output tokens, previous_response_id continuity, and a privacy-preserving safety_identifier hashed from the workspace and user IDs. When server-side history grows past about 100k tokens (or a context-window error is hit), older messages are summarized with gpt-4o-mini. MetricsHub tools are grouped into namespaces of fewer than ten functions; ListHosts and SearchHost stay immediately callable, larger schemas, PromQL and knowledge-base writes are loaded through hosted tool search on demand. Large MCP outputs are uploaded as files so Code Interpreter can analyze the complete result.

Self-hosted modes: common variables

ollama, vllm and openai-compatible read the same AI_* variables. AI_PROVIDER is a preset: it fixes the defaults and the backend quirks (Ollama’s sidecar vision, vLLM’s strict chat template and single-model adoption). OPENAI_API_KEY is not required and nothing is ever sent to OpenAI.

Variable Default Description
AI_BASE_URL Ollama http://localhost:11434/v1, vLLM http://localhost:8000/v1, required for openai-compatible Endpoint serving /v1/responses. Inside a container, localhost is the bot container
AI_API_KEY ollama / vllm / none Bearer token; the SDK requires one even when the server ignores it
AI_MODEL Ollama qwen3.8:27b; others: the single served model Chat model. With several served models (a gateway) it is required; the bot never guesses and refuses to start without one
AI_CONTEXT_LENGTH detected, else 32768 with a warning The model’s context window in tokens as the server allocates it (see below)
AI_MAX_OUTPUT_TOKENS 4000 Output cap per turn. In vLLM and openai-compatible modes, a value leaving the prompt fewer than 8k tokens is flagged at startup
AI_REQUEST_TIMEOUT_MS 300000 Per-request timeout
AI_MAX_TOOL_OUTPUT_CHARS ~40% of the usable budget Inline cap (chars) for one tool result before truncation/staging

Context length detection. In Ollama mode the bot reads the server’s effective context from the native API (/api/ps for a loaded model, /api/show for a Modelfile num_ctx); in vLLM and openai-compatible modes it reads max_model_len / context_length from /v1/models when reported. An unset AI_CONTEXT_LENGTH adopts the detected value; a smaller configured value is kept (tighter budgets are safe); a larger one is capped to the server’s value with a warning. The bot can never believe it has more context than the server allocates. Most gateways report nothing: set it explicitly there.

Deprecated aliases. The former OLLAMA_* / VLLM_* names of these settings (OLLAMA_MODEL, VLLM_BASE_URL, …) still work for their preset and take precedence over AI_*; the bot lists them once at startup with their replacements. OLLAMA_CONTEXT_LENGTH is a permanent alias of AI_CONTEXT_LENGTH, named after Ollama’s own server variable so one line configures both sides. OLLAMA_CONTEXT_WINDOW is no longer read.

Ollama preset (AI_PROVIDER=ollama)

Variable Default Description
OLLAMA_CONTEXT_LENGTH unset Permanent alias of AI_CONTEXT_LENGTH; must match Ollama’s num_ctx
OLLAMA_VISION_MODEL unset Sidecar vision model that describes image attachments as text (e.g. qwen3-vl:8b-instruct-8k). Unset = images surfaced as “cannot analyze”
OLLAMA_VISION_MAX_OUTPUT_TOKENS 600 Cap on the description; the vision model’s own (often 8k) context must hold image + prompt + output
AI_PROVIDER=ollama
AI_BASE_URL=http://ollama-host:11434/v1
AI_MODEL=qwen3.8:27b
AI_EMBEDDING_MODEL=nomic-embed-text
OLLAMA_CONTEXT_LENGTH=32768
OLLAMA_VISION_MODEL=qwen3-vl:8b-instruct-8k

On the Ollama host: ollama pull qwen3.8:27b, ollama pull nomic-embed-text and optionally ollama pull qwen3-vl:8b-instruct-8k.

The main chat model is text-only and Ollama’s /v1/responses has no image input, so screenshots are handled by the vision model through /v1/chat/completions: the bot downloads the image from Slack, asks for a factual description (verbatim text, chart trends, anomalies) with a short snippet of the conversation for focus, and injects that description as text. Descriptions are stored in the per-thread conversation, so each image is described once.

vLLM preset (AI_PROVIDER=vllm)

No vLLM-specific variables. The preset turns on native image input and strict input conforming, adopts the single served model when AI_MODEL is unset, and requires /v1/models at the health check. Embeddings need a dedicated endpoint (a vLLM instance serves one model), see the knowledge base section. Screenshots go through the media store, see the images section.

AI_PROVIDER=vllm
AI_BASE_URL=http://vllm-host:8000/v1
AI_API_KEY=vllm_...                    # whatever the reverse proxy in front of vLLM expects
# AI_MODEL=                            # adopted automatically
# AI_CONTEXT_LENGTH=                   # detected from max_model_len
M8B_MEDIA_BASE_URL=https://bot-host.example.com/m8b-media
# AI_EMBEDDING_BASE_URL=http://vllm-host:8001/v1
# AI_EMBEDDING_MODEL=qwen3-embedding-0.6b

OpenAI-compatible preset (AI_PROVIDER=openai-compatible)

Variable Default Description
AI_IMAGE_INPUT false The model reads images natively (input_image items). Then configure the media store as in vLLM mode
AI_STRICT_INPUT false One leading system message only, assistant history as plain strings. Enable on 400 System message must be at the beginning
AI_PROVIDER=openai-compatible
AI_BASE_URL=https://inference.example.com/v1
AI_API_KEY=...
AI_MODEL=llama-3.3-70b-instruct       # required when the gateway serves several models
AI_CONTEXT_LENGTH=131072              # gateways rarely report it
# AI_EMBEDDING_MODEL=nvidia/nv-embedqa-e5-v5

The endpoint must implement /v1/responses with streaming and function tools; /v1/chat/completions alone is not enough. The health check calls GET /v1/models: AI_MODEL must be listed there when the endpoint exposes it. A gateway without /v1/models passes with a warning (model unverified); authentication or server errors on it fail the check. Only universally supported request fields are sent (model, input, tools, stream, max_output_tokens); OpenAI-only fields never are. vLLM works through this preset too: it is openai-compatible + AI_IMAGE_INPUT=true + AI_STRICT_INPUT=true.

System prompt

The prompt (ai/config/system-prompt.js) is deployment-neutral and ships with the bot: persona, safety rules (no fabrication, action boundaries, credential handling) and tool guidance are the same for every installation and are not configurable. Two things adapt it to your organization.

Variable Default Description
M8B_PROMPT_EXTRA_FILE unset Markdown/text file appended as a delimited “Deployment notes” section. Relative paths resolve against the working directory; an unreadable file stops the bot
M8B_PROMPT_EXTRA unset Inline notes appended after the file’s content

Changes take effect on restart, including for open threads: in OpenAI mode each reply carries a fingerprint of the prompt it ran under and a thread started under a different prompt is re-seeded with the current one; self-hosted modes always resend the prompt. The bot logs the resolved organization and the size of the appended notes.

Knowledge base

OpenAI mode uses hosted file_search on the vector stores listed in OPENAI_VECTOR_STORE_IDS. Self-hosted modes use a local RAG store: Markdown documents in M8B_DATA_DIR/knowledge/docs/ are chunked, embedded through /v1/embeddings and persisted in knowledge/index.json; retrieval is cosine similarity, exposed as search_knowledge_base, and update_knowledge writes new entries into the same store. This flat-file design is intentional for a small corpus.

Variable Default Description
AI_EMBEDDING_MODEL Ollama nomic-embed-text; others unset Embedding model. Unset = knowledge base disabled (neither search nor writes are offered to the model)
AI_EMBEDDING_BASE_URL AI_BASE_URL Embedding endpoint. Required in vLLM mode (a vLLM instance serves one model): a second instance or an Ollama server
AI_EMBEDDING_API_KEY AI_API_KEY Bearer token for the embedding endpoint
AI_EMBEDDING_QUERY_PREFIX per model Task prefix for searches (nomic: search_query:; mxbai: query instruction). Set both prefixes empty to send none
AI_EMBEDDING_DOCUMENT_PREFIX per model Task prefix for indexing (nomic: search_document:)
AI_EMBEDDING_QUERY_INPUT_TYPE query for NVIDIA nv-embed*/embedqa Per-request input_type some APIs require
AI_EMBEDDING_DOCUMENT_INPUT_TYPE passage for NVIDIA models Same, when indexing
KNOWLEDGE_BASE_DIR M8B_DATA_DIR/knowledge Override of the store location

In vLLM and openai-compatible modes the embedding endpoint is probed once at startup (warning only); in Ollama mode a missing embedding model is only noticed on the first search, so pull it before starting (the doctor script checks it in every mode). A failing embedding call at runtime makes search_knowledge_base report the knowledge base as unavailable for that call; nothing else breaks.

Migrating an OpenAI vector store (the source documents live only in OpenAI): export, switch .env to the self-hosted backend (the indexer uses the configured provider and exits in OpenAI mode), then index:

docker compose run --rm --no-deps -e OPENAI_API_KEY=sk-... -e OPENAI_VECTOR_STORE_IDS=vs_... \
  m8b node scripts/export-openai-knowledge.js
# after switching .env to the self-hosted backend:
docker compose run --rm --no-deps m8b node scripts/index-knowledge.js

Indexing is incremental where it can be: bot-written knowledge is embedded at write time; node scripts/index-knowledge.js /var/lib/m8b/knowledge/docs/<file>.md [...] re-embeds only the listed documents; a full run is required after adding or removing many documents, or whenever you switch embedding models, prefixes or input types (the index refuses mixed-configuration writes and searches with a clear “rebuild” error). The index is written through a temporary file, so an interrupted run keeps the previous index. The running bot caches the index in memory: recreate the container after a rebuild.

Run docker compose run --rm --no-deps m8b node scripts/index-knowledge.js then docker compose up -d --force-recreate m8b (see Docker Compose): restart keeps the old container’s environment, so a rebuild that follows an embedding change would leave the bot on the previous model and every search rejected as a configuration mismatch.

Official MetricsHub documentation

A full index-knowledge.js run (local providers only — OpenAI mode uses hosted file_search and this script does not populate vector stores) first downloads the official documentation, then rebuilds the combined index, so a fresh installation answers installation, configuration, connector and troubleshooting questions with citations without any manually supplied document:

Retrieval tips: for a bilingual corpus bge-m3 usually beats nomic-embed-text; curate the corpus, since cosine similarity has no notion of “deprecated” (delete legacy documents or mark them with an explicit first line such as **Status: DEPRECATED — replaced by X**).

Images and the media store

Applies to vLLM mode and to openai-compatible with AI_IMAGE_INPUT=true. Screenshots go to the model natively as input_image items. Because the local conversation history is resent on every turn, images should not be embedded as base64 in it (without M8B_MEDIA_BASE_URL they are, and every later turn resends them). With a configured media store the bot downloads the image from Slack once (with its own Slack credentials, never given to the inference host), saves it under M8B_MEDIA_DIR as <uuid>.<ext>, and stores only the URL M8B_MEDIA_BASE_URL/<uuid>.<ext> in the conversation. A reverse proxy you run serves that directory to the inference host.

Variable Default Description
M8B_MEDIA_BASE_URL unset Public base URL of the media directory as seen from the inference host. Unset = base64 fallback (dev only, startup warning)
M8B_MEDIA_DIR M8B_DATA_DIR/media Where images are stored
M8B_MEDIA_RETENTION_DAYS 7 Age-based cleanup, swept at startup and every 6 hours
M8B_MEDIA_MAX_FILE_BYTES 10485760 Oversized images degrade to an explicit note

Only PNG, JPEG, GIF and WebP are passed through. If a stored conversation still references a deleted image, the reference is replaced by a text marker before the request is sent; threads rebuilt from Slack history re-download their images. The bot itself does not serve the media directory over HTTP:

# NGINX on the bot host: expose the media dir to the inference host ONLY
location /m8b-media/ {
    alias /var/lib/m8b/media/;
    allow <inference-host-IP>;
    deny all;
    autoindex off;
}
# vLLM systemd unit: restrict remote media fetching to the bot host (anti-SSRF)
# and enable the bounded media-download cache
# vllm serve ... --allowed-media-domains bot-host.example.com
Environment=VLLM_MEDIA_URL_ALLOW_REDIRECTS=0
Environment=VLLM_MEDIA_CACHE=/var/lib/vllm/media-cache
Environment=VLLM_MEDIA_CACHE_MAX_SIZE_MB=4096
Environment=VLLM_MEDIA_CACHE_TTL_HOURS=24

Python sandbox (self-hosted modes)

The hosted Code Interpreter is replaced by a run_python function tool backed by Pyodide (CPython in WebAssembly, in a worker thread). The WASM boundary is the sandbox: no network, no host filesystem except /data, a per-execution mount holding the inputs staged by the app (user attachments and large tool outputs as JSON). /outputs collects files posted back to the thread. numpy, pandas, matplotlib and openpyxl load on demand, downloaded once by the host and cached. Pyodide’s js interop module is stripped at startup and every execution is bounded by a hard timeout enforced with worker.terminate().

Variable Default Description
CODE_SANDBOX_ENABLED true false removes the tool; the prompt then asks for file contents inline
CODE_SANDBOX_TIMEOUT_MS 60000 Wall-clock limit per execution (the first import of a package downloads it)
CODE_SANDBOX_PACKAGE_CACHE_DIR M8B_DATA_DIR/sandbox/packages Downloaded Python packages
CODE_SANDBOX_MAX_OUTPUT_FILE_BYTES 20971520 Total size of the files one execution may deliver to Slack
CODE_SANDBOX_MAX_INPUT_FILE_BYTES 104857600 Cap per user attachment staged into /data (streamed to disk, not buffered)
CODE_SANDBOX_STAGING_DIR M8B_DATA_DIR/sandbox/staging Attachment download cache + per-execution /data directories
CODE_SANDBOX_STAGING_CACHE_MAX_BYTES 2147483648 Cache size before least-recently-used files are evicted

Web search and page reader (self-hosted modes)

OpenAI mode uses the hosted web search, which also reads pages. Self-hosted modes get an app-side web_search tool (titles and snippets) and a fetch_url tool that reads the URLs users paste and search results in full: the site’s Markdown rendition first (Accept: text/markdown, sibling .md, /llms.txt), GitHub issues and pull requests anonymously through the REST API (public repositories only, no tokens), otherwise the HTML page reduced to Markdown with Turndown after dropping navigation, scripts and asides. Only public http/https hosts are read: private, loopback, link-local and cloud-metadata addresses are refused at DNS resolution, redirects included. Binary responses (PDFs included) are refused; long pages are staged for run_python.

Variable Default Description
WEB_SEARCH_PROVIDER unset searxng or ollama-cloud. Unset/empty = web search unavailable. Compose defaults it to searxng
SEARXNG_URL unset SearXNG instance with JSON responses enabled (Compose: http://searxng:8080)
OLLAMA_CLOUD_API_KEY unset Key for ollama-cloud. Search queries leave your network (ollama.com)
FETCH_URL_ENABLED true false removes the page reader
FETCH_URL_ALLOWED_HOSTS unset Comma-separated host suffixes (example.com also matches docs.example.com). Empty = any public host
FETCH_URL_BLOCKED_HOSTS unset Same syntax; blocked wins
FETCH_URL_TIMEOUT_MS 20000 Whole-tool network timeout
FETCH_URL_MAX_BYTES 2097152 Per-response size cap after decompression

MetricsHub MCP servers

M8B connects to each MetricsHub agent through that agent’s MCP endpoint (URL + API token from the agent administrator). These settings configure MetricsHub agents; they do not enable arbitrary third-party MCP integrations.

Variable Default Description
M8B_MCP_CONFIG unset Path to a JSON array of servers (see below). Highest precedence
MCP_SERVERS unset The same JSON array inline (use literal ${VAR} placeholders, quote against shell/Compose expansion)
MCP_AGENT_URL unset Single-server mode: endpoint URL (https://<agent-host>:31888/sse, or /mcp on MetricsHub versions serving Streamable HTTP). Requires MCP_AGENT_TOKEN
MCP_AGENT_TOKEN unset Single-server mode: API token
MCP_AGENT_LABEL hostname Single-server mode: label the bot uses for the agent (defaults to the hostname of MCP_AGENT_URL)
MCP_AGENT_TRANSPORT probed http (Streamable HTTP, typically /mcp) or sse (legacy, typically /sse)
MCP_ALLOW_SELF_SIGNED_CERT false Single-server mode: skip certificate verification (prefer trusted certificates)
MCP_HOSTS_REFRESH_INTERVAL_MS 3600000 Periodic refresh of the consolidated host map (ListHosts/SearchHost); also after config changes. 0 disables

Precedence: M8B_MCP_CONFIG > MCP_SERVERS > legacy ai/mcp.config.local.js (deprecated) > MCP_AGENT_URL/MCP_AGENT_TOKEN. The first configured source wins; empty variables count as unset; [] disables MCP. Labels must be unique, URLs must be HTTP(S), every server needs a token, and ${ENV_VAR} references in token values must resolve to nonempty variables. Invalid configuration stops startup with an entry-specific error rather than skipping servers.

[
	{
		"server_label": "lab-a",
		"server_url": "https://metricshub-a.example.com/sse",
		"token": "${MCP_LAB_A_TOKEN}",
		"transport": "sse"
	},
	{
		"server_label": "lab-b",
		"server_url": "https://metricshub-b.example.com/mcp",
		"token": "${MCP_LAB_B_TOKEN}",
		"transport": "http",
		"allowSelfSignedCert": false
	}
]

When transport is omitted, M8B tries HTTP first, then SSE at the same URL if initialization returns HTTP 400/404/405, an unsupported content type, or times out after 10 seconds; it remembers the selected transport for reconnects. Authentication failures never trigger fallback. MCP and MetricsHub REST requests reject redirects (so credentials are never forwarded) and ask for the final URL. Server connection failures are logged but do not stop the bot; check the startup logs for Connected to <label> using <transport>. Configuration is loaded at startup: restart after any change. For a container, mount the JSON file and use its path inside the container.

Configuration editing

The bot can edit MetricsHub configuration files and request credentials through the agent’s REST API (same origin and token as MCP), with Slack approval buttons and credential dialogs.

Variable Default Description
METRICSHUB_CONFIG_ADMINS unset Comma-separated Slack user IDs allowed to request configuration changes. Empty = editing disabled
METRICSHUB_INTERACTION_TIMEOUT_MS 900000 How long a credential dialog or approval button stays valid

Prometheus

The PromQL tool works with any backend that serves the Prometheus HTTP API (/api/v1/query and /api/v1/query_range under the configured base URL): Prometheus, Mimir, Thanos, VictoriaMetrics, Grafana Cloud Metrics, a Grafana data source proxy, Dash0, New Relic’s PromQL endpoint…

Variable Default Description
M8B_PROMETHEUS_URL unset Base URL for the PromQL tool, path prefix included (Grafana Cloud: .../api/prom, Dash0: .../api/prometheus). Unset = no tool
M8B_PROMETHEUS_BASIC_AUTH unset user:password sent as HTTP Basic auth. Grafana Cloud: metrics instance ID and an access policy token with metrics:read
M8B_PROMETHEUS_HEADERS unset JSON object of extra headers, e.g. {"Authorization":"Bearer ..."} (Grafana data source, Dash0) or {"X-Query-Key":"..."} (New Relic)

Datadog does not speak PromQL and is not covered by this tool.

Persistent storage

Variable Default Description
M8B_DATA_DIR ./data Root of all local stores, relative to the working directory. The Docker image sets /var/lib/m8b
Subdirectory Override Used by
knowledge/ KNOWLEDGE_BASE_DIR Local knowledge base (with an embedding model)
media/ M8B_MEDIA_DIR Native image input without provider uploads
sandbox/packages/ CODE_SANDBOX_PACKAGE_CACHE_DIR Python sandbox
sandbox/staging/ CODE_SANDBOX_STAGING_DIR Python sandbox
conversations/ reserved Future conversation persistence

Startup creates and write-checks only the stores used by enabled features; OpenAI mode needs none. An unwritable active store fails startup with its path. Ensure the bot user owns the directories; if using the NGINX media proxy, point its alias at the resolved media directory. When upgrading from a version whose sandbox caches lived under node_modules/.cache, stop the bot and move the data (nothing is moved automatically) or keep the per-store overrides pointing at the old locations.

Logging

Variable Default Description
NODE_ENV unset production: warnings and errors. test: info and above. Anything else: debug (verbose)

Info/debug logs include reasoning summaries, tool activity, token/cache usage and the complete LLM response. Treat development logs as potentially sensitive operational data.

Deployment notes

Docker Compose

The image is published at docker.metricshub.com and requires MetricsHub customer registry credentials. Follow the installation steps to pull the image, create the Slack app and .env.

The supplied docker-compose.yml and searxng/settings.yml are bundled in the image under /app/deploy; the installation steps copy them into your installation directory. Keep the searxng subdirectory next to the Compose file so its relative bind mount resolves correctly.

docker-compose.yml pulls docker.metricshub.com/metricshub/m8b-slack:latest (customer credentials required; docker login docker.metricshub.com). Set M8B_IMAGE_TAG in .env to the image tag you installed to pin a version. Upgrade with docker compose pull then docker compose up -d; docker compose down preserves data, -v deletes it.

The container runs as the non-root node user (UID/GID 1000) with a named volume at /var/lib/m8b; Compose fixes M8B_DATA_DIR to that mount. Back up this volume. Bind mounts must be writable by UID/GID 1000. Use backend hostnames reachable from the container in AI_BASE_URL and MCP URLs. Socket Mode needs no inbound ports. No container health check is declared until the HTTP health endpoint (issue #13) exists. URL-based image delivery still needs the external media proxy above.

For a JSON MCP config or a deployment notes file, add a read-only bind mount and set M8B_MCP_CONFIG or M8B_PROMPT_EXTRA_FILE to its container path. Do not bake deployment files or credentials into the image. In Compose .env files, single-quote values containing literal $ (including JSON with ${TOKEN_NAME} placeholders) to prevent Compose interpolation.

The maintenance scripts (doctor.js, index-knowledge.js, export-openai-knowledge.js) run in a one-off container that reuses the image, the .env file and the data volume:

docker compose run --rm m8b node scripts/doctor.js
docker compose run --rm --no-deps m8b node scripts/index-knowledge.js
docker compose run --rm --no-deps -e OPENAI_API_KEY=sk-... -e OPENAI_VECTOR_STORE_IDS=vs_... \
  m8b node scripts/export-openai-knowledge.js

Arguments follow the script path (node scripts/index-knowledge.js --skip-docs-download), -e adds one-off variables, and --no-deps skips starting SearXNG, which only the doctor’s web search probe needs. docker compose exec m8b ... works too when the bot is already running.

Compose starts a private SearXNG service (JSON enabled, no published port) and defaults WEB_SEARCH_PROVIDER=searxng / SEARXNG_URL=http://searxng:8080; OpenAI mode keeps its hosted search. searxng/settings.yml is a first-start template: SearXNG generates a secret and stores its active settings in the searxng-config volume, so later template edits do not apply. To customize an existing installation:

docker compose cp searxng:/etc/searxng/settings.yml ./searxng-active.yml
# edit, then
docker compose cp ./searxng-active.yml searxng:/etc/searxng/settings.yml
docker compose restart searxng

To use an external SearXNG, set SEARXNG_URL in .env (the instance must allow JSON responses), start only the bot with docker compose up -d --no-deps m8b and stop the bundled service, or remove the searxng service and the depends_on entry. WEB_SEARCH_PROVIDER= (empty) disables app-side web search.

Diagnostics

docker compose run --rm m8b node scripts/doctor.js

Checks the Slack tokens and granted scopes against manifest.json, connects to every MCP server (tools and host counts, warns when a server reports no hosts), verifies the AI backend with a real streaming /v1/responses call offering a function tool, probes the embeddings endpoint and the web search backend when configured, write-tests the data directories and boots the Python sandbox. Exit code 1 when any check fails; no secrets are printed.