Repository navigation
Expand file tree
/
Copy path.env.example
More file actions
777 lines (741 loc) · 42.5 KB
/
Copy path.env.example
File metadata and controls
777 lines (741 loc) · 42.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
# Required UNLESS you run entirely on local or OpenRouter models (see
# LOCAL_MODELS_ENABLED / OPENROUTER_ENABLED below). At least one provider must
# be configured or the app refuses to start.
ANTHROPIC_API_KEY=sk-ant-your-key-here
# Only for a key issued at the ORGANISATION level rather than inside a
# workspace. Those keys are rejected with HTTP 400 unless every request carries
# the workspace id. Leave unset for a workspace-scoped key.
# ANTHROPIC_WORKSPACE_ID=
# --- Auth (UI: Auth.js / NextAuth v5 with Google and/or SSO) ----------------
# Trying it alone? Leave AUTH_GOOGLE_ID and AUTH_OIDC_ISSUER blank: `make dev` then uses local
# login — no sign-in, reachable only from this computer, and you
# are the owner (docs/auth.md). Fill in this block to invite your team or to
# run it on a server; `make docker` always needs it.
# Generate AUTH_SECRET with: openssl rand -base64 32
# AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET come from a Google Cloud OAuth Client
# (Web application). Authorized redirect URIs must include:
# - http://localhost:3000/api/auth/callback/google
# - https://<your-ui-host>/api/auth/callback/google
# ALLOWED_EMAILS = comma-separated list of Google-account emails permitted to
# sign in. Always honored, and always sufficient on its own. People in the
# backend roster (non-archived Person rows with an email) are allowed IN
# ADDITION to this list, never instead of it — so roster changes cannot
# lock out an operator listed here. Any other email is rejected, even with
# a valid Google login.
# AUTH_TRUST_HOST = "true" is required when running behind a reverse proxy.
# AUTH_URL = public origin of the UI. LEAVE BLANK for local dev (localhost is
# inferred). Required on any public deployment; AUTH_TRUST_HOST alone
# isn't enough — without AUTH_URL the post-callback redirect lands on the
# container's bind address (0.0.0.0) instead of the public hostname,
# e.g. AUTH_URL=https://exec.example.com
AUTH_SECRET=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# SSO sign-in through any OpenID Connect provider (Keycloak, Okta, Auth0,
# Authentik, Microsoft Entra ID, …), alongside Google or instead of it.
# Register a web client with a secret at the provider, redirect URI
# https://<your-ui-host>/api/auth/callback/oidc. All three are needed.
# The provider must mark emails verified (docs/auth.md, "SSO sign-in").
# AUTH_OIDC_ISSUER=https://sso.example.com/realms/company
# AUTH_OIDC_ID=
# AUTH_OIDC_SECRET=
# AUTH_OIDC_NAME=SSO # the button: "Sign in with SSO"
# Only if the provider can't mark emails verified AND people can't change
# their own email there (docs/auth.md):
# AUTH_OIDC_TRUST_UNVERIFIED_EMAIL=false
ALLOWED_EMAILS=alice@example.com,bob@example.com
AUTH_TRUST_HOST=true
AUTH_URL=
# Shared secret between the Next.js proxy and the FastAPI backend. The UI
# attaches this as `x-api-key`; the API requires it on every non-exempt
# request. Leave unset for `make dev` (gate off); set it to the same random
# value on both apps in any deployed environment. Generate with:
# openssl rand -hex 32
BACKEND_SHARED_SECRET=
# Signed callers (docs/auth.md). The shared secret proves a request came from
# the UI, not who is signed in. With these set, the UI signs who is calling
# and the API checks the signature; without them, the API takes the UI's word.
# Recommended on any server. Make the pair once with:
# uv run --with cryptography python scripts/make-caller-keys.py
# The UI only — keep it as secret as the shared secret:
CALLER_ASSERTION_PRIVATE_KEY=
# The API only (comma-separated while rotating a key):
CALLER_ASSERTION_PUBLIC_KEYS=
# Comma-separated extra CORS origins for the FastAPI backend; the `make dev`
# UI on localhost:3000 is allowed too unless OE_PUBLIC_DEPLOYMENT is set.
# Deployment-only — LEAVE BLANK for local dev: with the shared-secret gate
# unset, listing a public origin here lets scripts on that origin read your
# local API with credentials.
# On deploy: BACKEND_ALLOWED_ORIGINS=https://exec.example.com
BACKEND_ALLOWED_ORIGINS=
# Declares this instance as internet-reachable. Set to 1 on every public
# deployment: the API then REFUSES TO START without BACKEND_SHARED_SECRET,
# instead of serving an unauthenticated API with only a log warning. Leave
# blank for local dev. Any value other than 0/false/no/off arms the check.
OE_PUBLIC_DEPLOYMENT=
# Model configuration (defaults shown)
DEFAULT_MODEL=claude-sonnet-5-5
DEEP_REASONING_MODEL=claude-sonnet-5-5
ROUTING_MODEL=claude-haiku-5-5
# Data paths (defaults shown — relative to repo root, override with absolute paths if needed)
# VECTOR_STORE_PATH=./chroma_db
# COMPANY_PROFILE_PATH=./company/profile.yaml
# EPISODIC_DB_PATH=./episodic_memory.db # SQLite for episodic memory, alerts, knowledge review
# Feature flags
ENABLE_CACHING=true
# --- OpenRouter routing -------------------------------------------------------
# Toggle that routes Claude calls through OpenRouter (so usage is billed to
# your OpenRouter account) AND unlocks the curated set of non-Anthropic
# models (GPT-5, Gemini 2.5 Pro, Llama, DeepSeek, Grok) in the Council UI
# dropdown so any agent can be flipped to a non-Anthropic model per-agent.
# Off by default — a fresh checkout's behavior is identical to before.
OPENROUTER_ENABLED=false
# Required when OPENROUTER_ENABLED=true. Get one at https://openrouter.ai.
OPENROUTER_API_KEY=
# Live model catalog. With OpenRouter enabled the API fetches OpenRouter's
# public /models list at startup (and every OPENROUTER_CATALOG_REFRESH_S) to
# fill the non-Anthropic entries of the Council dropdown, so new releases
# show up without a code change. Filtered to tool-capable, paid, text-only
# models from the vendors below — newest N per vendor. A failed fetch keeps
# the built-in fallback list. Defaults shown.
# OPENROUTER_CATALOG_ENABLED=true
# OPENROUTER_CATALOG_PROVIDERS=openai,google,anthropic,meta-llama,deepseek,x-ai
# OPENROUTER_CATALOG_PER_PROVIDER=6
# OPENROUTER_CATALOG_TIMEOUT_S=10
# OPENROUTER_CATALOG_REFRESH_S=21600
# --- Local / self-hosted models ----------------------------------------------
# Route selected model slugs to a local OpenAI-compatible server (Ollama,
# LM Studio, vLLM, llama.cpp, …) instead of the Anthropic API. Off by default;
# a fresh checkout behaves identically to before.
# Also works with HOSTED OpenAI-compatible gateways: point LOCAL_BASE_URL at
# the gateway's /v1 endpoint, set LOCAL_API_KEY, and list its model slugs in
# LOCAL_MODELS. See README → "Using a hosted OpenAI-compatible gateway".
LOCAL_MODELS_ENABLED=false
# Base URL of the local server, INCLUDING the version path. Required when
# LOCAL_MODELS_ENABLED=true.
# Ollama http://localhost:11434/v1
# LM Studio http://localhost:1234/v1
# vLLM http://localhost:8000/v1
LOCAL_BASE_URL=
# Optional bearer token. Ollama / LM Studio need none (leave blank); vLLM or a
# gateway in front may require one.
LOCAL_API_KEY=
# Comma-separated model slugs to surface in the Council UI and route locally.
# Sent to the server verbatim, so they must match what it serves
# (e.g. `ollama list`). Example: llama3.3,qwen2.5:14b
LOCAL_MODELS=
# Local generation (especially on CPU) can be slow — generous timeout (seconds).
LOCAL_TIMEOUT_S=300
# Off by default. The `usage: {include: true}` request field is an
# OpenRouter-only cost-accounting extension, not part of the OpenAI or
# Anthropic request schema — a plain self-hosted server (Ollama, vLLM) or a
# strict gateway that forwards nearly verbatim to real Anthropic rejects an
# unrecognized top-level field outright instead of ignoring it (every local
# call returns 400). Only enable this if you've confirmed your LOCAL_BASE_URL
# backend actually speaks OpenRouter's request format — some hosted, billed
# gateways do, and then this restores real USD cost tracking on the
# /audit cache_event rows for those calls.
LOCAL_INCLUDE_USAGE_ACCOUNTING=false
# Optional reasoning_effort sent on every local request. Thinking-only
# models (e.g. GLM on Fireworks) can otherwise spend the whole max_tokens
# budget reasoning and return no answer/tool call. Unset = not sent.
# One of none, minimal, low, medium, high (anything else fails at startup).
# LOCAL_REASONING_EFFORT=low
#
# To run with NO Anthropic key, configure the three lines above AND point the
# model settings at local slugs:
# DEFAULT_MODEL=llama3.3
# DEEP_REASONING_MODEL=llama3.3
# ROUTING_MODEL=llama3.3
# (Server-side web search has no local equivalent and is disabled for local
# models; tool-use quality depends on the model you choose.)
# The eval judge (`make eval`, the Evals page) is Claude Opus unless this
# names another model; a slug from LOCAL_MODELS judges on the local server.
# EVAL_JUDGE_MODEL=llama3.3
# --- Chat turn timeouts -------------------------------------------------------
# Whole-turn wall-clock ceiling for a streaming /chat turn, in seconds. A turn
# that exceeds it is cut off and whatever streamed so far is persisted. Users
# can also end a turn themselves with the Stop button (POST /chat/stop), which
# is what makes a ceiling this generous reasonable.
# CHAT_STREAM_TIMEOUT_S=300
# Extra allowance added on top when a message opts in to Committee review, so
# 360s in total at the defaults.
# COMMITTEE_EXTRA_TIMEOUT_S=60
# How often a streaming /chat turn sends an SSE keepalive comment while nothing
# else is going out, so a browser or proxy doesn't drop it during a slow tool
# call. 0 turns it off.
# CHAT_STREAM_KEEPALIVE_S=15
# Ceiling on one call through the MCP gateway (Gmail, Calendar, Drive, Outlook,
# OneDrive). A call past it is abandoned and reported as not answered.
# MCP_TOOL_TIMEOUT_S=120
# Per-call ceiling for the onboarding interview. Kept separate from the chat
# ceiling: the wizard retries twice, so a 300s value here would mean a 10-minute
# hang before the user sees a timeout.
# INTERVIEW_TIMEOUT_S=120
# --- Update check -------------------------------------------------------------
# Settings shows the running version and, when a newer release is out on
# GitHub, a link to it. The API asks GitHub's public releases API at most every
# few hours and sends nothing about this install. Set to false for an
# air-gapped install or one that must not call out.
UPDATE_CHECK_ENABLED=true
# --- Honcho per-person memory ------------------------------------------------
# External memory layer (https://honcho.dev) that gives the Executive a
# per-person peer card shared across Slack, Discord, Telegram, email, and the
# web UI. When enabled, each turn does one prefetch + fire-and-forget
# sync_turn keyed off the inbound user's Person.id; turns with no matching
# Person row pass through with no Honcho call. Works against hosted Honcho
# (leave HONCHO_BASE_URL empty) or a self-hosted instance (set it). Off by
# default — a fresh checkout behaves identically to before.
HONCHO_ENABLED=false
# Required when HONCHO_ENABLED=true.
HONCHO_API_KEY=
# Self-hosted Honcho endpoint, e.g. http://honcho.internal:8000
HONCHO_BASE_URL=
# Workspace id scopes peer cards within a Honcho deployment. Keep stable
# across environments unless you intentionally want isolated memory.
HONCHO_WORKSPACE_ID=openexec
# Hard timeout on the per-turn prefetch. On timeout the turn proceeds with
# whatever the builtin episodic block provides (no peer_memory injected).
HONCHO_PREFETCH_TIMEOUT_S=3.0
# How the per-turn prefetch reads the person's memory. `representation`
# (default) reads the peer card and the derived conclusions most relevant to
# the inbound message: a GET with no LLM behind it, ~100 ms. `dialectic` asks
# Honcho a reasoned question instead (an LLM call, seconds; `reasoning_level`
# applies). Set `dialectic` for a Honcho server without the peer-context route.
HONCHO_PREFETCH_MODE=representation
# Conclusions retrieved per turn in representation mode (1-100).
HONCHO_PREFETCH_MAX_CONCLUSIONS=20
# Override if you're proxying OpenRouter or running an Azure-style gateway.
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
# OPENROUTER_APP_TITLE="Open Executive"
# OPENROUTER_REFERER=https://your-deployment.example.com
# OPENROUTER_TIMEOUT_S=180.0
# Web search — Anthropic's native server-side web_search tool. The application
# default is ON (unset means enabled); this file ships it off so a fresh setup
# incurs no per-search charges until you opt in. Set true to let the Executive
# and specialists combine your uploaded documents with live lookups (market
# data, news, regulatory changes, competitor moves).
ENABLE_WEB_SEARCH=false
WEB_SEARCH_MAX_USES=5
# Optional allowlist OR blocklist (comma-separated bare domains, no scheme).
# Set at most one of the two.
# WEB_SEARCH_ALLOWED_DOMAINS=
# WEB_SEARCH_BLOCKED_DOMAINS=
# Slack integration (optional). Setting BOTH tokens starts the Socket Mode
# listener as part of the API's own startup — `make dev` brings Slack up with
# the rest of the app, no separate process. Leave either unset to disable
# inbound Slack (outbound sends only need SLACK_BOT_TOKEN).
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
# Or, on a server Slack can reach, hear Slack over HTTP instead of Socket Mode:
# leave SLACK_APP_TOKEN unset, set the app's Signing Secret here, and point the
# app's Event Subscriptions Request URL at https://<this API>/webhook/slack.
# SLACK_SIGNING_SECRET=
# Exec email address — REQUIRED. The Executive's own mailbox: the Google
# account authenticated with Gmail MCP OAuth, or the Microsoft 365 mailbox
# signed in below (see EMAIL_PROVIDER). No default: the Executive must never
# operate from a fallback address because EXEC_EMAIL_ADDRESS silently fell through.
EXEC_EMAIL_ADDRESS=exec@example.com
# AI disclosure: mail and chat the Executive sends end with a line saying it
# is an AI assistant (orchestrator/disclosure.py). On by default. The texts
# replace the built-in wording and must say "AI", or the built-in one is used.
# AI_DISCLOSURE=true
# AI_DISCLOSURE_MAIL_TEXT=
# AI_DISCLOSURE_CHAT_TEXT=
# Where each person's own switches start for replies sent as them under Act as
# me: those Handle it for me sends on its own (default on), and drafts they
# approve and send (default off). Each person can change theirs.
# AI_DISCLOSURE_HANDLE_IT=true
# AI_DISCLOSURE_APPROVED_SENDS=false
# How often the email poller checks for new messages (seconds, default 60)
EMAIL_POLL_INTERVAL_SECONDS=60
# Whole-turn ceiling for the Executive's answer to one inbound email (seconds).
# A message that runs past it is marked read and not tried again.
# EMAIL_TURN_TIMEOUT_S=300
# Channel access (Email/Telegram/Discord) is roster-driven: only senders
# whose channel ID matches a non-archived Person row receive a response.
# Manage access via the /people UI; no env-var allowlists.
# Someone off the roster who writes in is held for the principal to confirm
# ("is this Annamarie? add her") and gets one fixed "received, awaiting
# confirmation" reply. Limits (defaults shown):
# ROSTER_ACK_WINDOW_DAYS=7 # one acknowledgement per sender per window
# ROSTER_ACK_DAILY_CAP=20 # acknowledgements a day, all senders
# ROSTER_REQUEST_DAILY_CAP=30 # new requests a day
# ROSTER_REQUEST_TTL_DAYS=14 # an unanswered request closes after this
# Telegram integration (optional)
# Setup: docs/telegram_setup.md
# Bot token from @BotFather
TELEGRAM_BOT_TOKEN=
# Must equal the secret_token passed to setWebhook, or every update is rejected
# with 401. 1-256 chars of A-Z a-z 0-9 _ - (e.g. `openssl rand -hex 32`).
TELEGRAM_WEBHOOK_SECRET=
# Discord integration (optional)
# Setup: create an application at https://discord.com/developers/applications
# Enable the "Message Content" privileged intent. Invite with bot + applications.commands scopes.
# Setting DISCORD_BOT_TOKEN starts the bot inside the API — `make dev` brings it
# up, no separate process. Don't also run `make discord` while the API has the
# token: two gateway connections on one token answer every message twice.
# Bot token from the Discord Developer Portal → Bot tab
DISCORD_BOT_TOKEN=
# Application ID (General Information tab)
DISCORD_APP_ID=
# comma-separated guild (server) IDs for instant slash-command registration during dev; leave blank for global
DISCORD_GUILD_IDS=
# default channel ID for outbound notifications/alerts (optional)
DISCORD_NOTIFY_CHANNEL_ID=
DISCORD_THREAD_RESPONSE_GATE_ENABLED=true # in bot-owned threads, ask Haiku whether each non-mention message warrants a reply (filters side chatter in multi-party threads); set to false to reply to every thread message
# Google Chat integration (optional)
# Numeric project number (not the project ID) — see docs/google_chat_setup.md
GOOGLE_CHAT_PROJECT_NUMBER=
# Auth — set ONE of the following (see docs/google_chat_setup.md for which to use):
# Option A: absolute path to SA JSON key (blocked by some org policies)
GOOGLE_CHAT_SERVICE_ACCOUNT_FILE=
# Option B: SA email for impersonation via ADC (gcloud auth application-default login)
GOOGLE_CHAT_SERVICE_ACCOUNT_EMAIL=
# Google Workspace MCP (optional — enables Gmail, Calendar, Drive access)
# Outbound email is sent via Gmail MCP using the account authenticated below.
# Setup: https://github.com/taylorwilsdon/google_workspace_mcp
# 1. Create OAuth Desktop credentials at console.cloud.google.com
# Enable: Gmail API, Google Calendar API, Google Drive API
# 2. Set these two vars, then mint a token (workspace-mcp has no `auth`
# subcommand — OAuth fires on the first tool call):
# uv run --with mcp python scripts/mint-google-token.py
# Sign in as EXEC_EMAIL_ADDRESS. Credentials land in
# .gworkspace-credentials/ (gitignored).
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
# Which Google services the co-located workspace-mcp child loads, and how many
# tools per service. Defaults load everything; narrow them only if you want a
# smaller tool surface.
#
# WORKSPACE_MCP_TOOLS "all" (default), or comma-separated service names:
# gmail, calendar, drive, docs, sheets (also chat,
# forms, slides, tasks, contacts, search, appscript).
# A service left out is never imported, so none of
# its tools exist — the Connected Systems prompt
# block narrows to match, and the Executive is not
# told about tools it cannot call.
# WORKSPACE_MCP_TOOL_TIER "complete" (default), "extended", or "core".
# Tiers are cumulative; "core" is the smallest set
# and leaves out ten of the pinned tools (Gmail
# drafts/threads/attachments, Drive item listing,
# freebusy, and the Docs/Sheets listing tools).
#
# Narrowing either one is a prompt-visible change: fewer services are named in
# block 0 and fewer tool names are pinned. Leave both unset for every service
# at the complete tier.
WORKSPACE_MCP_TOOLS=all
WORKSPACE_MCP_TOOL_TIER=complete
# Microsoft 365 MCP (optional — enables Outlook mail + calendar tools)
# Runs @softeria/ms-365-mcp-server co-located in the API image, the Microsoft
# analogue of the Google Workspace server above; both can be configured at
# once. The Executive discovers its tools (microsoft_365__send-mail,
# microsoft_365__create-calendar-event, …) via search_tools; recipients and
# attendees are limited to your People roster by the gateway's egress gate.
# Setup (Microsoft Entra admin center → App registrations → New):
# 1. Supported account types: your org (single tenant) — or personal/any for
# a consumer mailbox. Authentication → "Allow public client flows: Yes".
# 2. API permissions (Delegated, Microsoft Graph): Mail.ReadWrite, Mail.Send,
# Calendars.ReadWrite, plus User.Read and offline_access for the sign-in —
# then grant admin consent. The launcher starts the server with an explicit
# allow-list of Outlook mail + calendar tools (no inbox rules, mailbox
# settings, calendar sharing or Teams), and those are the scopes that list
# requests. To print exactly what it asks for, inside the API container:
# MS365_MCP_CLIENT_ID=<id> /usr/local/bin/ms365-mcp-launch.sh --list-permissions
# 3. Copy the mcp_servers.json.example `microsoft_365` entry into
# company/mcp_servers.json, set the vars below, then sign in ONCE with the
# device-code flow (prints a URL + code; sign in as EXEC_EMAIL_ADDRESS):
# docker compose exec api /usr/local/bin/ms365-mcp-launch.sh --login
# docker compose exec api /usr/local/bin/ms365-mcp-launch.sh --verify-login
# Local dev without Docker: MS365_MCP_SERVER_BIN is run as a single
# program path (no arguments), so point it at an executable wrapper, e.g.
# .ms365-credentials/ms365-server.sh containing
# #!/bin/sh
# exec npx -y @softeria/ms-365-mcp-server@0.154.2 "$@"
# (chmod +x it), then:
# MS365_MCP_USE_KEYTAR=0
# MS365_MCP_TOKEN_CACHE_PATH=$PWD/.ms365-credentials/.token-cache.json
# MS365_MCP_SERVER_BIN=$PWD/.ms365-credentials/ms365-server.sh \
# sh docker/ms365-mcp-launch.sh --login
# The refresh token persists under MS365_MCP_CREDENTIALS_DIR
# (/data/ms365_credentials in Docker; .ms365-credentials/ is gitignored).
# Sign-in is only ever this --login: the gateway refuses the server's
# login / logout / select-account / remove-account tools from chat (they
# are registered whatever the tool allow-list says) and hides them from
# search_tools; verify-login / list-accounts stay readable.
# Application (client) ID of your Entra app — required for Microsoft 365.
# MS365_MCP_CLIENT_ID=
# Directory (tenant) ID; "common" for multi-tenant, "consumers" for personal.
# MS365_MCP_TENANT_ID=
# Only if the app registration is confidential.
# MS365_MCP_CLIENT_SECRET=
# Pin the signed-in account to this UPN (recommended: EXEC_EMAIL_ADDRESS).
# MS365_MCP_EXPECTED_USERNAME=
# Work/school tenant features (shared mailboxes); off for personal accounts.
# MS365_MCP_ORG_MODE=1
# OneDrive tools in chat (and the file permissions for the OneDrive folder
# sync). Off by default: turning it on asks for Files.ReadWrite and
# Files.Read.All, so run --login again afterwards, or every Microsoft 365
# tool fails until you do. See docs/onedrive_sync_setup.md.
# MS365_MCP_ONEDRIVE=true
# Workspace provider switch (optional; defaults keep everything on Google).
# The three code paths that call a FIXED mailbox/calendar — the inbound email
# poller, alert email dispatch + the scheduler's email hint (EMAIL_PROVIDER),
# and the typed create_calendar_event / create_instant_meeting tools + the
# approval-time conflict check (CALENDAR_PROVIDER) — run on the chosen
# backend. "google" needs the google_workspace MCP server, "microsoft" the
# microsoft_365 one. Separate switches: Outlook mail + Google Calendar is fine.
# A missing server is logged at startup and the poller skips; boot never fails.
# EMAIL_PROVIDER=google # google | microsoft
# CALENDAR_PROVIDER=google # google | microsoft
# Act as me (optional) — the Executive drafts replies AS YOU, in your own
# Drafts (Gmail or Outlook), when you ask it to or for mail in your inbox that needs you. It
# sends nothing unless you tap Send on a reply waiting on Today, which on a
# server needs signed callers (CALLER_ASSERTION_*, above). Only the owner can
# turn it on, in Settings → Act as me. It uses a separate credential for YOUR mailbox,
# never the Executive's, and never through the MCP gateway. Mint it once with
# the same OAuth client as above, signed in as yourself:
# uv run --with google-auth-oauthlib python scripts/connect-own-gmail.py --email you@example.com
# or, for an Outlook mailbox, with the Microsoft 365 app above (MS365_MCP_CLIENT_ID,
# public client flows on, delegated User.Read, Mail.ReadWrite, Mail.Send) exported:
# uv run python scripts/connect-own-outlook.py --email you@example.com
# then put the file it writes in this directory (Docker: /data/delegation_google).
# One file per person: connecting the other kind replaces it.
# Add --calendar to either script to also grant read-only calendar access, for
# Read my calendar in Settings → Act as me (Google: calendar.readonly on the
# OAuth client's consent screen; Outlook: delegated Calendars.Read on the app).
# DELEGATION_GOOGLE_CREDENTIALS_DIR=./company/delegation_google
# Model that learns "How I write" and writes the drafts (default: DEFAULT_MODEL).
# DELEGATION_COMPOSER_MODEL=
# Ceiling on drafts written as one person per UTC day (1-1000).
# DELEGATION_MAX_DRAFTS_PER_DAY=50
# "Draft replies to my inbox" (Settings → Act as me): how often it checks the
# inbox, how many of the day's drafts it may write, and the model that decides
# whether an email needs a reply (unset: ROUTING_MODEL).
# DELEGATION_INBOX_POLL_MINUTES=5
# DELEGATION_INBOX_MAX_DRAFTS_PER_DAY=20
# DELEGATION_CLASSIFIER_MODEL=
# Handle it for me (Settings → Act as me): the most replies the inbox watcher
# may send on its own as one person per UTC day (1-500), within its drafts limit.
# DELEGATION_HANDLE_IT_MAX_SENDS_PER_DAY=20
# Let the owner turn Act as me on for team members too (each connects their
# own Gmail or Outlook and turns it on for themselves; Settings → Act as me). Default off.
# DELEGATION_TEAM_MEMBERS=false
# MCP gateway (the client side — external MCP servers surfaced as Executive
# tools, including the Google Workspace one configured above).
# NOTE: this is the gateway only. Exposing Open Executive itself AS an MCP
# server to other agents is a separate feature that needs none of these vars:
# it is served at /mcp by the API and gated by BACKEND_SHARED_SECRET above.
# See the "Connect as an MCP Server" section of README.md.
# Left unset, MCP turns itself on when MCP_SERVERS_CONFIG_PATH exists, so
# copying mcp_servers.json.example into place is all that is needed. Set it
# explicitly to override that either way: MCP_ENABLED=false keeps MCP off with
# the file still in place, MCP_ENABLED=true is a no-op unless the file defines
# at least one server under "mcpServers".
# A config that defines no servers, a path that is not a readable file, or a
# gateway that fails to start (or hangs) is not fatal — the API boots without
# MCP tools (and without the email poller) and logs the reason at ERROR.
# MCP_ENABLED=false
# MCP_SERVERS_CONFIG_PATH defaults to ./company/mcp_servers.json, relative to
# the working directory. Docker keeps it on the /data volume instead and sets
# the var explicitly — see docs/deployment.md.
# Folders a workflow action step's oe__read_file tool may read — where tools
# that download files (e.g. Gmail attachments via workspace-mcp) save them.
# Comma-separated. Default: $WORKSPACE_ATTACHMENT_DIR, else
# ~/.workspace-mcp/attachments (workspace-mcp's own default).
# WORKFLOW_FILE_DIRS=
# An action step may also act through one short sandboxed script (run_script,
# workflows/step_script.py) that calls only the step's own tools, each through
# the same allowlist, budget, target check and audit. false: tools one by one.
# WORKFLOW_STEP_SCRIPTS=true
# A step's script calls have their own budget: 10x its max_tool_calls, up to
# this many (a script's calls cost no model turn each).
# WORKFLOW_SCRIPT_MAX_CALLS=500
# The same in chat, over the gateway tools a conversation has found (each call
# checked as a call_tool). Never offered on a turn private to the principal.
# CHAT_SCRIPTS=true
# The most tool calls one chat turn's scripts may make in all (messages,
# people and alerts have tighter caps of their own).
# CHAT_SCRIPT_MAX_CALLS=200
# How many scripts may run at once across the server (each a sandbox worker
# of up to 128 MB and one CPU core while computing); more wait their turn.
# SCRIPT_MAX_WORKERS=2
# Python jobs (run_python_job): Python with real libraries on files, in a
# WebAssembly sandbox the API image installs at /opt/pysandbox. Off: not offered.
# PYTHON_JOBS_ENABLED=true
# PYTHON_SANDBOX_DIR=/opt/pysandbox
# PYTHON_JOB_TIMEOUT_S=120
# PYTHON_JOB_MEMORY_MB=1536
# Run jobs on a separate runner instead of the local sandbox: each job is
# POSTed there with the key as a bearer token, and the runner sends back the
# result the local sandbox would. https only (plain http to localhost,
# .internal or .flycast hosts). The runner must isolate jobs itself.
# PYTHON_JOB_RUNNER_URL=
# PYTHON_JOB_RUNNER_KEY=
# Libraries the runner has beyond the local sandbox's, named to the model so it
# uses them (only with a runner), e.g. scipy, scikit-learn (import sklearn)
# PYTHON_JOB_EXTRA_LIBRARIES=
# Python modules this install adds, comma-separated: each module's register()
# adds chat tools and kinds of document (orchestrator/extensions.py).
# OPENEXECUTIVE_EXTENSIONS=
# A script that works may be saved as a named tool and run again (run_script
# save_as / tool=, workflows/saved_tools.py; Settings -> Advanced -> Saved
# tools). It can only call what the chat or step running it may. false: no
# saving and no saved tool runs.
# SAVED_TOOLS_ENABLED=true
# Scanned PDFs (optional tuning)
# A PDF with no text layer (a scan, or one printed to PDF as images) is OCR'd on
# this server by default. With PDF_PROVIDER_READING=true it is instead read by
# the deployment's model through its provider's own PDF support: Anthropic
# natively; OpenRouter via its file-parser (models that read files natively
# get "native", others PDF_OPENROUTER_ENGINE: mistral-ocr | cloudflare-ai);
# a local server only with LOCAL_PDF_INPUT=true (e.g. LOCAL_BASE_URL pointed at
# https://api.openai.com/v1). Otherwise the server OCRs the pages locally.
# The model that reads them; unset means DEFAULT_MODEL.
# PDF_VISION_MODEL=
# PDF_OPENROUTER_ENGINE=mistral-ocr
# LOCAL_PDF_INPUT=false
# Opt-in. Off (the default), scanned PDFs never leave this server: local OCR
# reads them. On, they go to the provider as described above (on OpenRouter
# possibly through its third-party parser).
# PDF_PROVIDER_READING=false
# PDF_VISION_MAX_PAGES=100
# PDF_VISION_PAGES_PER_CALL=20
# PDF_OCR_ENABLED=true
# Files a channel delivers (chat, Slack, Google Chat, email attachments):
# pages converted per file, and across all senders per rolling hour.
# PDF_INBOUND_MAX_PAGES=30
# PDF_INBOUND_PAGES_PER_HOUR=300
# Outbound DM anti-spam guard (optional tuning)
# Applied at the send-tool chokepoint: a proactive Slack/Telegram/Discord DM is
# suppressed (and audited) when it would exceed the per-recipient rate cap,
# duplicate a recently-sent message, or land while the recipient is on leave /
# outside their availability windows. Defaults shown.
OUTBOUND_MAX_PER_RECIPIENT_PER_WINDOW=5 # max DMs to one recipient per rate window
OUTBOUND_RATE_WINDOW_MINUTES=60 # rolling window for the rate cap
OUTBOUND_DEDUP_WINDOW_MINUTES=360 # suppress a near-identical resend within this window
OUTBOUND_RESPECT_QUIET_HOURS=true # honor recipient availability windows / on-leave; set false to disable
# Overnight client rotation (multi-client practice mode). OFF by default:
# rotating activates each parked client at the configured time (UTC) to
# generate its brief + run monitors — conscious opt-in (LLM cost per client
# per night + automated context switching).
# CLIENT_ROTATION_ENABLED=true
# CLIENT_ROTATION_TIME=03:30
# Notion → isolated wiki-collection sync (optional). OFF by default. Create
# an internal integration at https://www.notion.so/my-integrations, share
# the company wiki (and only that) with it, then:
# NOTION_SYNC_ENABLED=true
# NOTION_API_KEY=ntn_...
# Pages land in the notion_wiki collection (not company_docs) and are
# labelled unreviewed at retrieval. Un-shared pages are purged on the
# next tick. Manual: openexecutive sync-notion / purge-notion
# NOTION_SYNC_ENABLED=false
# NOTION_API_KEY=
# NOTION_SYNC_INTERVAL_MINUTES=60
# NOTION_MAX_PAGES_PER_SCAN=40
# Google Drive folder → isolated collection sync (optional). OFF by default.
# Reads as a service account with drive.readonly: share the folders to sync
# with the service account's email (and with the Executive's Google account,
# so it can open a synced file live). Setup: docs/drive_sync_setup.md.
# Files land in the drive_docs collection (not company_docs) and are
# labelled unreviewed at retrieval. Files removed from the folders are
# purged on the next tick. Manual: openexecutive sync-drive / purge-drive
# DRIVE_SYNC_ENABLED=false
# DRIVE_SYNC_SERVICE_ACCOUNT_FILE=/secrets/drive-sync-sa.json
# DRIVE_SYNC_FOLDER_IDS=folderId1,folderId2
# DRIVE_SYNC_INTERVAL_MINUTES=60
# DRIVE_MAX_FILES_PER_SCAN=40
# OneDrive folder → isolated collection sync (optional). OFF by default.
# Reads as the Executive's Microsoft 365 sign-in (the microsoft_365 MCP
# server's, which must include Files.Read.All); only the folders listed here
# are synced. Each entry is <drive id>/<item id>: run
# openexecutive onedrive-folder <folder sharing link>
# to get one. Setup: docs/onedrive_sync_setup.md. Files land in the
# onedrive_docs collection (not company_docs) and are labelled unreviewed at
# retrieval. Manual: openexecutive sync-onedrive / purge-onedrive
# ONEDRIVE_SYNC_ENABLED=false
# ONEDRIVE_SYNC_FOLDERS=driveId/itemId,driveId/itemId
# ONEDRIVE_SYNC_INTERVAL_MINUTES=60
# ONEDRIVE_MAX_FILES_PER_SCAN=40
# MS365_MCP_LAUNCHER=/usr/local/bin/ms365-mcp-launch.sh
# Confluence space → isolated collection sync (optional). OFF by default.
# Cloud and Server/Data Center. The token acts as the Confluence user who
# made it; only the spaces listed are synced, and pages with read
# restrictions (their own or a parent page's) are skipped unless
# CONFLUENCE_SYNC_SKIP_RESTRICTED=false. The URL, credential and SSL names
# match mcp-atlassian's, so one set serves both. Setup:
# docs/confluence_sync_setup.md. Pages land in the confluence_wiki collection
# (not company_docs) and are labelled unreviewed at retrieval. Manual:
# openexecutive sync-confluence / purge-confluence
# CONFLUENCE_SYNC_ENABLED=false
# Cloud: https://<site>.atlassian.net/wiki
# CONFLUENCE_URL=https://confluence.example.com
# Server/Data Center: a personal access token
# CONFLUENCE_PERSONAL_TOKEN=
# Cloud: your Atlassian email and an API token
# CONFLUENCE_USERNAME=
# CONFLUENCE_API_TOKEN=
# CONFLUENCE_SYNC_SPACE_KEYS=ENG,OPS
# true, false, or the path of a CA bundle for a private certificate authority
# CONFLUENCE_SSL_VERIFY=true
# CONFLUENCE_SYNC_SKIP_RESTRICTED=true
# CONFLUENCE_SYNC_ALLOW_HTTP=false
# Refuse a CONFLUENCE_URL that resolves to a private or local address
# CONFLUENCE_SYNC_PUBLIC_HOSTS_ONLY=false
# CONFLUENCE_SYNC_INTERVAL_MINUTES=60
# CONFLUENCE_MAX_PAGES_PER_SCAN=40
# Alert lifecycle — keeps the briefing's "Needs you" queue and the daily
# briefs from going stale (alerts/lifecycle.py, alerts/review.py). Defaults
# shown; all optional. An unread alert older than its category TTL is
# expired by a scheduler sweep (reversible: POST /alerts/{id}/reopen).
# Monitoring = unrouted low/medium watchlist signals; action = everything a
# human should look at. 0 disables expiry for that category.
ALERT_TTL_DAYS_MONITORING=3
ALERT_TTL_DAYS_ACTION=14
# Executive alert review: every ALERT_REVIEW_INTERVAL_HOURS (and right
# before the morning brief) the Executive re-examines open alerts with
# evidence and, within department authority, routes + DMs the owner,
# nudges, escalates to the principal, drafts an artifact, folds
# duplicates, or resolves with evidence (only on high confidence). Every
# move is audited and reversible. ALERT_REVIEW_MAX_MOVES_PER_SCAN=0 keeps
# the review but makes it annotate-only (no DMs / drafts).
ALERT_REVIEW_ENABLED=true
ALERT_REVIEW_INTERVAL_HOURS=6
ALERT_REVIEW_MIN_AGE_HOURS=2
ALERT_REVIEW_MAX_PER_SCAN=25
ALERT_REVIEW_MAX_MOVES_PER_SCAN=10
# Daily briefs: when nothing changed since the last delivered brief, send a
# one-line "nothing new" instead of re-synthesising the same list.
PRINCIPAL_BRIEF_SUPPRESS_UNCHANGED=true
# Grounding checks on unattended prose: the morning brief, EOD digest,
# reflection and alert text may name only people and figures found in their
# own inputs or the company profile. enforce = hold back / refuse what can't
# be grounded, report = audit only, off = skip. GROUNDING_CITATIONS adds [n]
# after each grounded figure in a brief and a Sources list at the end.
GROUNDING_CHECKS=enforce
GROUNDING_CITATIONS=true
# The /today "What's going on" header: every N minutes between the two local
# hours, the scheduler rewrites the principal's header if anything moved
# (new mail, a held reply, a proposal, the hour). 0 = only when the page opens.
BRIEFING_NARRATIVE_REFRESH_MINUTES=10
BRIEFING_NARRATIVE_REFRESH_START_HOUR=6
BRIEFING_NARRATIVE_REFRESH_END_HOUR=22
# Your time zone (IANA, e.g. America/New_York). A zone set in the app
# (PUT /workspace) wins; this is the fallback, then UTC. The morning brief
# (08:00), end-of-day digest (18:00) and reflection (07:30) fire at those
# times in that zone. Setting one of the three times below pins it instead,
# read as UTC (HH:MM) — the behaviour from before time zones existed; a
# value that is not a valid HH:MM is ignored.
# USER_TIMEZONE=UTC
# PRINCIPAL_BRIEF_MORNING_TIME=
# PRINCIPAL_BRIEF_EOD_TIME=
# PRINCIPAL_REFLECTION_TIME=
# Solo mode only: the weekly review goes out Friday 16:00 in the user's zone.
# Set a whole weekly spec (weekly@DOW@HH:MM, e.g. weekly@fri@16:00) to pin it,
# read as UTC like the times above; anything else is ignored.
# PRINCIPAL_WEEKLY_REVIEW_TIME=
# Stop re-chasing the same stalled item forever (delivered nudges per item;
# 0 disables the cap).
NUDGE_MAX_PER_SCOPE=3
# Attunement — per-person open loops. Commitments ("I'll send it Thursday") and
# asks from anyone on the roster become open loops that the nudge engine
# chases once due; "sent it" from the same person closes them.
# ATTUNEMENT_ENABLED=true
# ATTUNEMENT_OPEN_LOOPS_ENABLED=true
# ATTUNEMENT_LOOP_DEFAULT_DUE_DAYS=2
# ATTUNEMENT_LOOP_TTL_DAYS=21
# ATTUNEMENT_MAX_OPEN_LOOPS_PER_PERSON=15
# Someone assigned a task by someone else is told once, on their own channel
# (their quiet hours apply). false keeps assignments silent until the first chase.
# ATTUNEMENT_ASSIGN_NOTIFY_ENABLED=true
# On the first weekday of each month the owner gets one message counting what
# the Executive did last month (Pulse -> Your month has the full view).
# VALUE_SUMMARY_MONTHLY_ENABLED=true
# ATTUNEMENT_MAX_CALLS_PER_DAY=200
# Outcome ledger: proactive DMs are resolved replied / acted, or ignored after
# ATTUNEMENT_IGNORE_AFTER_HOURS. A person whose last MUTE_MIN_SENDS resolved
# sends from a nudge source all went unanswered is chased less for it.
# ATTUNEMENT_IGNORE_AFTER_HOURS=72
# ATTUNEMENT_MUTE_MIN_SENDS=5
# ATTUNEMENT_MUTE_COOLDOWN_MULTIPLIER=3
# Do what it says: a reply that claims a change no tool made ("Done") gets one
# more round to make it or say it can't. One small ROUTING_MODEL call per such reply.
# CLAIM_CHECK_ENABLED=true
# Conversation review: once a chat has been quiet QUIET_MINUTES, one small tool
# loop reads its new part and fixes what the replies left undone (closes, cancels,
# moves, marks sent, learns style), only from the person's own quoted words.
# Never sends, books or deletes. Empty model = ROUTING_MODEL.
# CHAT_REVIEW_ENABLED=true
# CHAT_REVIEW_QUIET_MINUTES=30
# CHAT_REVIEW_MAX_PER_DAY=10 # per person
# CHAT_REVIEW_MODEL=
# Replay of the misses: nightly at CHAT_REPLAY_TIME (user's zone), the turns the
# review graded a miss run again with today's lessons, in a dry run (reads only;
# every send, booking or save is recorded, not made), and a judge says whether
# each would now go right. Empty judge model = EVAL_JUDGE_MODEL, else Sonnet.
# CHAT_REPLAY_ENABLED=true
# CHAT_REPLAY_TIME=03:30
# CHAT_REPLAY_MAX_CASES=30
# CHAT_REPLAY_MINUTES=20
# CHAT_REPLAY_JUDGE_MODEL=
# Working style: up to 8 short "how they like replies" rules per person, learned
# from their own thumbs up/down and requests. Re-learned after TRIGGER_TURNS new
# messages (or right after a thumbs-down), at most once per MIN_INTERVAL_HOURS
# and MAX_PER_DAY times a day per person.
# ATTUNEMENT_STYLE_ENABLED=true
# ATTUNEMENT_STYLE_TRIGGER_TURNS=10
# ATTUNEMENT_STYLE_MIN_INTERVAL_HOURS=2
# ATTUNEMENT_STYLE_MAX_PER_DAY=4
# Research council cadence: the periodic run is considered every
# WATCHLIST_RESEARCH_INTERVAL_MINUTES and skipped when nothing changed.
WATCHLIST_RESEARCH_INTERVAL_MINUTES=360
# Force a research-council re-run at most this often when internal state is
# unchanged (was daily; a daily forced run re-filed the same findings).
WATCHLIST_RESEARCH_MAX_STALENESS_HOURS=168
# Web searches each research specialist may run per run (separate from
# WEB_SEARCH_MAX_USES, which chat and standing queries use).
RESEARCH_WEB_SEARCH_MAX_USES=3
# Specialists the research run fans out to (comma-separated slugs from
# cso, cfo, cmo, coo, chro, cpo, gc). Empty = all seven.
# RESEARCH_SPECIALISTS=cso,cfo,coo
# Research watchlist policy: the research pass only proposes watches. A
# proposal grounded in company data (a named competitor / vendor / ticker /
# initiative) and corroborated is added on its own, quietly (at most
# MAX_DIRECT_ADDS per run); anything the Executive is not sure about lands
# as a dry-run SUGGESTION on /watchlist for you to approve or decline (at
# most MAX_PROPOSALS per run). Above WATCHLIST_MAX_ENABLED enabled watches
# every add becomes a suggestion; suggestions nobody reviews within
# WATCHLIST_PROPOSAL_TTL_DAYS are removed (re-proposable after 90 days).
WATCHLIST_RESEARCH_MAX_DIRECT_ADDS=2
WATCHLIST_RESEARCH_MAX_PROPOSALS=2
WATCHLIST_MAX_ENABLED=40
WATCHLIST_PROPOSAL_TTL_DAYS=14
# RAG relevance gates (cosine distance — LOWER is stricter; range 0-2).
# A hit further than the gate is dropped before the top-K slice.
#
# KNOWLEDGE_DISTANCE_THRESHOLD is the shared gate: COMPANY docs, the synced
# Notion wiki, and recent research. Raise it if company documents that
# obviously answer the question are not being surfaced — a question phrased
# conversationally embeds further from document prose than the same question
# phrased as keywords, so the right doc can rank first and still miss the gate.
#
# KNOWLEDGE_BUILTIN_DISTANCE_THRESHOLD optionally tightens the built-in
# knowledge base and the shipped failure cases only. Both are generic material
# and much larger than a typical company corpus, so a gate loose enough for
# your documents admits a lot of unrelated handbook prose with it. It can only
# tighten: a value looser than the shared gate is clamped to it. Unset, every
# collection shares the value above.
KNOWLEDGE_DISTANCE_THRESHOLD=0.55
# KNOWLEDGE_BUILTIN_DISTANCE_THRESHOLD=0.50
# How many knowledge chunks each specialist gets per question: built-in
# knowledge, and company documents (the company count also caps each synced
# source: Notion, Drive, OneDrive, Confluence). Every chunk is sent to the
# model, so higher counts cost more per answer.
# SPECIALIST_BUILTIN_N_RESULTS=3
# SPECIALIST_COMPANY_N_RESULTS=2
# Longest single tool result the model sees, in characters (about 4 per
# token). A longer one is cut with a note asking for a narrower request.
# TOOL_RESULT_MAX_CHARS=20000
# What all of one question's tool results may add up to, in characters. Past
# it, each further result shows its first 3,000. 0 turns it off.
# TOOL_RESULTS_TURN_MAX_CHARS=40000