-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagents.toml.example
More file actions
511 lines (459 loc) · 24.7 KB
/
agents.toml.example
File metadata and controls
511 lines (459 loc) · 24.7 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
# ── phantom-mesh agents.toml ──────────────────────────────────────────────────
# Copy this file to the appropriate location and fill in your API keys.
#
# Config file locations (in order of precedence):
# 1. Path given via --config flag to the daemon
# 2. ./agents.toml (current working directory)
# 3. macOS: ~/Library/Application Support/com.phantommesh.app/agents.toml
# 4. Linux: ~/.config/phantom-mesh/agents.toml
#
# NEVER put real API keys in this file.
# Always use api_key_env to reference an environment variable instead:
# export ANTHROPIC_API_KEY=sk-ant-...
# Full documentation: docs/GETTING-STARTED.md
# ──────────────────────────────────────────────────────────────────────────────
# ── Core daemon settings ───────────────────────────────────────────────────────
[core]
# host: address the HTTP daemon binds to
# "0.0.0.0" — listen on all interfaces (required for Tailscale/cluster access)
# "127.0.0.1" — local-only (more secure for single-machine use)
host = "0.0.0.0"
# port: HTTP port the daemon listens on (default: 7878)
port = 7878
# hub_api_key: optional API key required to call this node's HTTP API.
# When set, all inbound requests must include:
# Authorization: Bearer <hub_api_key>
# Leave commented out to allow unauthenticated local access.
# hub_api_key = "change-me-random-string"
# ── LLM Providers ─────────────────────────────────────────────────────────────
# Configure at least one provider. The agent tries the primary provider first;
# if it fails (HTTP error, rate-limit, etc.) it automatically falls back to the
# next provider in the list.
#
# Each provider block supports these fields:
# type — "anthropic" | "openai_compat" | "gemini" | "groq"
# api_key_env — name of the env var holding the API key (preferred)
# api_key — inline API key (NOT recommended; use api_key_env instead)
# url — override the default API endpoint (required for Ollama,
# OpenRouter, and any OpenAI-compatible service)
# default_model — model used when the agent doesn't specify one explicitly
# tier — optional label ("free" | "paid") for routing logic
# ──────────────────────────────────────────────────────────────────────────────
[providers.anthropic]
# Anthropic Claude (direct API, OpenAI-compatible endpoint)
# Get a key: https://console.anthropic.com/settings/keys
# Note: the required `anthropic-version` header is added automatically when
# type = "anthropic" — no extra configuration needed.
type = "anthropic"
api_key_env = "ANTHROPIC_API_KEY" # export ANTHROPIC_API_KEY=sk-ant-...
default_model = "claude-sonnet-4-6"
[providers.openrouter]
# OpenRouter — access 200+ models with one key
# Get a key: https://openrouter.ai/keys
type = "openai_compat"
url = "https://openrouter.ai/api"
api_key_env = "OPENROUTER_API_KEY" # export OPENROUTER_API_KEY=sk-or-...
default_model = "google/gemini-2.5-flash"
[providers.gemini]
# Google Gemini (direct API)
# Get a key: https://aistudio.google.com/app/apikey
type = "gemini"
api_key_env = "GEMINI_API_KEY" # export GEMINI_API_KEY=AIza...
default_model = "gemini-2.5-flash"
[providers.groq]
# Groq — fast Llama/Mixtral inference
# Get a key: https://console.groq.com/keys
type = "groq"
api_key_env = "GROQ_API_KEY" # export GROQ_API_KEY=gsk_...
default_model = "llama-3.3-70b-versatile"
[providers.opencode]
# OpenCode Zen gateway — frontier models (Claude, GPT-5, Gemini-3) plus
# 7 free-tier / stealth models. The endpoint is `/zen/v1`, NOT `/api/v1`
# — the legacy `/api/v1` path returns a Cloudflare 404.
#
# Free / stealth models verified live 2026-05-15 via
# curl https://opencode.ai/zen/v1/models | jq
# (re-verify before listing — Zen rotates free-tier models frequently):
# big-pickle (stealth, no `-free` suffix)
# deepseek-v4-flash-free
# minimax-m2.5-free
# nemotron-3-super-free
# qwen3.6-plus-free
# ring-2.6-1t-free (was ling-2.6-flash-free, renamed)
# trinity-large-preview-free
#
# Note: hy3-preview-free is GONE; use trinity-large-preview-free or
# big-pickle as the modern preview slot. See docs/positioning.md §6.
type = "opencode"
api_key_env = "OPENCODE_API_KEY" # export OPENCODE_API_KEY=sk-...
default_model = "claude-sonnet-4-6"
[providers.ollama]
# Ollama — run local models (no API key needed)
# Install: https://ollama.com | pull a model: ollama pull llama3.2
type = "openai_compat"
url = "http://localhost:11434" # local Ollama; no API key needed
default_model = "llama3.2"
# ── Free-tier cloud providers (zero-cost, OpenAI-compatible) ─────────────────
# Recommended failover order when one rate-limits: Cerebras → Groq → NVIDIA NIM.
# See docs/FREE-LLM-PROVIDERS-2026-05.md for the full comparison + signup links.
[providers.cerebras]
# Cerebras Inference — wafer-scale, 1M tokens/day free, 30 RPM, 8K context cap
# Get a key: https://cloud.cerebras.ai (no CC required)
type = "openai_compat"
url = "https://api.cerebras.ai/v1"
api_key_env = "CEREBRAS_API_KEY"
default_model = "llama-3.3-70b"
# (Groq is already configured above — duplicate block removed 2026-05-15.)
[providers.nvidia]
# NVIDIA NIM — 100+ models incl. DeepSeek-R1 / Nemotron, credits-based
# Get a key: https://build.nvidia.com (Developer Program signup required)
type = "openai_compat"
url = "https://api.nims.nvidia.com/v1"
api_key_env = "NVIDIA_NIM_API_KEY"
default_model = "nemotron-3-super"
# ── Agents ────────────────────────────────────────────────────────────────────
# Define one or more named agents. The web UI and Telegram bot use "master" by
# default. You can add more agents (e.g. [agent.coder], [agent.researcher]) and
# reference them by name.
#
# Each agent block supports:
# provider — key under [providers.*] to use
# model — overrides the provider's default_model for this agent
# tools — list of tool names this agent may call (see full list below)
# instructions — system prompt injected at the start of every conversation
#
# Available tools:
# shell — run shell commands (git, cargo, npm, python, etc.)
# file_read — read a file's contents
# file_write — create or overwrite a file
# file_edit — replace an exact string in an existing file
# content_search — search file contents with ripgrep
# glob_search — find files matching a glob pattern
# web_search — search the web (Brave if api key set, else DuckDuckGo)
# memory_store — persist a key-value pair across sessions
# memory_recall — retrieve a previously stored value by key
# git_status — show working tree status (git status --short)
# git_diff — show diff stats for staged/unstaged changes
# git_log — show recent commit history
# git_commit — stage all tracked changes and create a commit
# ──────────────────────────────────────────────────────────────────────────────
[agent.master]
# Single primary provider (legacy / simple form):
provider = "anthropic"
# Optional explicit failover priority. If set, providers are tried in this
# exact order before falling through to `provider` above and then alphabetical
# of any remaining configured providers. Useful when you have multiple
# free-tier provider keys and want to control which absorbs traffic first.
#
# providers = ["groq", "cerebras", "anthropic", "openrouter"]
#
# Each entry must match a [providers.<name>] block. Missing keys (env var
# unset / api_key empty) are silently skipped. The runtime falls forward to
# the next provider on HTTP 4xx (non-retriable client errors) or after
# retry exhaustion on 429/5xx. See `core/src/agent.rs::resolve_provider_order`.
# Default (when omitted): legacy behavior — start with `provider`, then
# alphabetical of the rest.
model = "claude-sonnet-4-6" # overrides provider's default_model
tools = [
"shell",
"file_read",
"file_write",
"file_edit",
"content_search",
"glob_search",
"web_search",
"memory_store",
"memory_recall",
"git_status",
"git_diff",
"git_log",
"git_commit",
]
instructions = """
You are a senior software engineer AI assistant running on the user's machine.
You have direct access to the filesystem and shell via tools.
The single rule that overrides every other instinct: NEVER claim to have done
something without first calling a tool to do it, and NEVER report fabricated
outputs as if they were real.
ANTI-HALLUCINATION RULES (mandatory, no exceptions):
1. NO claim of action without a tool call.
"I created X / wrote Y / ran Z / fetched W" is forbidden unless the
IMMEDIATELY PRECEDING turn contained the matching tool call (file_write,
shell, etc.). If you intend to do something, call the tool first, see the
real result, then describe it.
2. Verify after writing.
After file_write or any state-changing shell command, the very next thing
you do must be a verification tool call (file_read of the same path, or
shell `ls -la`, `cat`, etc.). Quote the verification output back to the
user. If verification fails, report the failure plainly.
3. Real tool errors are never papered over.
If a tool returns non-zero exit, empty output, or an error message, that
IS the result. Surface it verbatim.
4. "I cannot do X because Y" is always allowed.
When a dependency is missing, permission denied, network unreachable, or
you simply do not know — say so directly. Short honest "no" beats long
fabricated "yes".
5. Never invent specifics.
Dates, timestamps, byte counts, file sizes, URLs, version numbers, model
IDs, latency numbers, news headlines, commit SHAs — if you did not get
them from a tool call in this turn, do not state them.
6. Today's date comes from the user/system, not your training memory.
For "today" or "current" queries, use shell `date` or ask the user.
Never infer from training-cutoff calendar memory.
7. Real-time data needs real fetches.
News, prices, weather, GitHub state, package versions — these come from
web_search / shell curl / etc. If no fetch tool is available, say so.
8. When the user calls you out, do not loop the lie.
If the user says "you faked X", stop. Identify what tool calls actually
happened versus what was claimed. Apologize once, briefly, then actually
run the tools — don't promise "this time I'll really do it" and repeat.
Tool quick reference:
- shell: run commands (git, cargo, npm, python, etc.)
- file_read: read files before editing
- file_edit: precise string replacements
- file_write: create new files
- content_search (ripgrep), glob_search
- git_* tools for version control
- web_search if available
Always respond in the user's language. Show tool output, then a tight
summary — no marketing copy.
"""
# ── Squad Pipeline agents (SPEC-FREEZE-V1 §11.1, §12.4) ────────────────────────
# These two agents power the `/dispatch <natural-language>` slash command.
# Only the coordinator (Mac M1 in the 5/9 demo) needs them; workers don't have
# to declare them. They use a cheap free-tier model on purpose: dispatcher
# emits structured JSON, synthesizer summarises peer outputs — neither is
# the heavy LLM lift.
[agent.dispatcher]
provider = "groq" # free tier, fast token rate
model = "llama-3.3-70b-versatile" # JSON-output capable, cheap
# No tools — dispatcher only reasons over peer inventory + emits text/JSON.
tools = []
instructions = """You are a Squad Pipeline dispatcher running on a phantom-mesh coordinator. Your sole job is to convert a user's natural-language goal into a structured dispatch plan that fans out to peer nodes.
INPUT YOU WILL RECEIVE:
- The user's goal (one or more sentences in any language)
- A live inventory of online peers and their available agents, e.g.:
node-a [Win, full] agents: [master, coder, recon, enrich, review]
node-b [Linux, full] agents: [master, coder, enrich]
node-c [iOS, sandbox file_in_container,memory,web,subagent] agents: [master, triage]
node-d [Win, full] agents: [master, coder, review]
OUTPUT FORMAT (strict — your reply must be PARSEABLE JSON, nothing else):
{
"plan": [
{"peer": "<peer_name>", "agent": "<agent_name>", "prompt": "<task for that peer>"}
],
"synthesis_hint": "<one-sentence brief for the synthesizer agent on how to combine peer outputs>"
}
DISPATCH RULES:
1. Pick at most 4 peers (one per OS family preferred for diversity).
2. Match agent name to task: recon-style work goes to a recon-agent, code review to review-agent, etc.
3. Sandbox peers (worker_caps non-empty) only get tasks that match their caps. Never dispatch shell/git work to a sandbox peer.
4. Each peer's prompt should be self-contained; peers don't see other peers' prompts.
5. If the user goal can be done by a single peer, that's fine — emit a 1-entry plan.
6. If no plan makes sense (e.g. goal is gibberish or all peers offline), emit `{"plan": [], "synthesis_hint": "<reason>"}`.
Output ONLY the JSON. No prose, no markdown fence, no commentary. Parsing failure = your job failure.
"""
[agent.synthesizer]
provider = "groq"
model = "llama-3.3-70b-versatile"
tools = []
instructions = """You are a Squad Pipeline synthesizer running on a phantom-mesh coordinator. After the dispatcher's plan has fanned out to N peers and each returned its agent's output, you receive the bundle and produce a unified report.
INPUT YOU WILL RECEIVE:
- The original user goal
- The dispatcher's `synthesis_hint` (one sentence on how outputs combine)
- N peer outputs, each tagged with peer name + agent name + elapsed_ms:
## Peer: node-a (recon-agent, 4123 ms)
<text from node-a's recon-agent>
## Peer: node-b (enrich-agent, 6010 ms)
<text from node-b's enrich-agent>
## Peer: node-c (triage-agent, 8804 ms)
<text from node-c's triage-agent>
OUTPUT FORMAT:
- A single markdown report with headings, lists, tables as appropriate
- The original user goal restated as a one-line ## title
- A TL;DR section summarising the answer in 2-3 sentences
- Sections for each peer's contribution (if heterogeneous) OR a unified narrative (if peers all worked on the same sub-problem)
- A "Confidence" or "Caveats" section flagging any peer that errored, took unusually long, or whose output disagrees with another peer's
CRITICAL:
- Do NOT invent content not in the peer outputs.
- If a peer returned an error, surface it as "(peer X reported: <err>)" rather than dropping it silently.
- Output is markdown ready for direct rendering in TerminalShell. No JSON wrapping.
"""
# ── Tool permissions (Tool(specifier) DSL) ────────────────────────────────────
# Phantom's permission engine gates tool execution per call. Three lists —
# deny / ask / allow — composed with first-match-wins, deny > ask > allow.
#
# Tool names: Specifier shapes:
# Bash / Shell → shell Bash(npm run *) — glob on `command` arg
# Read → file_read Read(./.env) — glob on `path` arg
# Write → file_write
# Edit → file_edit / file_write / multi_file_edit / apply_patch
# Edit(./src/**) — covers the edit-family
# WebFetch → web_fetch WebFetch(domain:github.com)
# WebSearch → web_search
# * → matches every tool name
#
# Bash hardening: any Allow rule matching a shell command that contains a
# redirect (>, >>, |, <) or chain operator (;, &&, ||) is auto-downgraded
# to Ask — closes the `cat * → cat secrets > /tmp/exfil` exfiltration hole.
#
# Numeric priority prefix overrides default action precedence:
# "100:Bash(git status)" ← high priority — beats a generic Deny on Bash
#
# Empty (or missing) [permissions] block = legacy allow-all. Once any rule
# is present, unmatched calls fall through to Ask. See docs/PERMISSIONS.md
# for the full reference + 3 worked policy templates.
# ──────────────────────────────────────────────────────────────────────────────
# [permissions]
# Uncomment the block + customize. Three example presets:
#
# ── Preset 1: "Personal dev mode" (most permissive) ──
# deny = ["Read(./.env)", "Read(./secrets/*)"]
# allow = ["*"]
#
# ── Preset 2: "Production-careful" (ask before writes) ──
# deny = ["Read(./.env)", "Bash(rm -rf *)"]
# ask = ["Edit", "Write", "Bash"]
# allow = [
# "Bash(git status)", "Bash(git diff)", "Bash(git log *)",
# "Bash(cargo check)", "Bash(cargo build)", "Bash(cargo test)",
# "Read(./*)",
# ]
#
# ── Preset 3: "CI auto-deny shell" (tightest sandbox) ──
# deny = ["Bash"]
# allow = ["Read(./*)", "Edit(./src/**)"]
#
# Verify with: phantom doctor (human-readable)
# phantom doctor --json | jq .permissions (machine-readable)
# ── Tools configuration ────────────────────────────────────────────────────────
# [tools]
# brave_search_api_key = "BSA..." # Get a free key at https://api.search.brave.com/
# # Without this, web_search falls back to DuckDuckGo.
# # Note: this is a plain string value, not an env var name.
# ── P2P Cluster / multi-node mesh (optional) ──────────────────────────────────
# Connect multiple phantom-mesh nodes over Tailscale to share work across machines.
# All nodes in a cluster must use the same cluster_secret value.
#
# Steps:
# 1. Install Tailscale on every node: https://tailscale.com
# 2. Run 'tailscale ip -4' on each machine to get its Tailscale IP
# 3. Fill in the peers list and set a strong shared cluster_secret
# 4. Start the daemon on each node
#
# See configs/agents.coordinator.toml, configs/agents.cloud.toml,
# configs/agents.worker.toml for role-specific templates.
#
# [cluster]
# node_name = "my-mac" # human-readable name for this node
# cluster_secret = "change-me-random-64-chars" # shared secret — same on all nodes
# peers = [
# "http://100.x.x.2:7878", # GCP cloud node (Tailscale IP)
# "http://100.x.x.3:7878", # worker node (Tailscale IP)
# ]
#
# capabilities: tags advertised to peers so the dispatcher can route tasks
# whose `--required-caps` mention them. Different from `worker_caps` (below):
# `capabilities` is a generic descriptor ("this box has a GPU"), `worker_caps`
# is the sandbox/whitelist used to *reject* mismatched dispatch. For most
# clusters set both to the same list; for the E001 cross-host smoke testbed
# the two nodes use disjoint values so each task must forward:
# # workstation:
# capabilities = ["gpu"]
# # Pi 4 / vision box:
# capabilities = ["vision"]
# Used by `core/src/mesh.rs::select_best_peer_with_caps`. See also
# `docs/superpowers/runbooks/E001-testbed-setup.md`.
# capabilities = []
#
# worker_caps: this node's worker capabilities — advertises what kinds of
# tasks this node is willing to accept. Other peers can target this node via
# `phantom peer assign --required-caps android,camera ...`; the dispatcher
# (see `core/src/mesh.rs::select_best_peer_with_caps`) filters peers whose
# advertised `worker_caps` is a superset of the task's `required_caps`. A
# mismatch surfaces as `DispatchError::NoPeerSatisfiesCaps`.
#
# Default empty (`[]`) = this node accepts any task (acts as a "full" worker).
# Set caps to filter, e.g.:
# worker_caps = ["android", "camera", "gps"] # on a phone
# worker_caps = ["full"] # on a workstation
# worker_caps = []
#
# C4 (v0.6.0): peer-heartbeat tuning. Only consulted by the daemon when
# built with `--features experimental-cluster-heartbeat`. When that
# feature is OFF (the v0.6.0 default), `select_best_peer_with_caps` never
# tier-demotes peers and these knobs are inert. When ON, the heartbeat
# task probes each peer's `/rpc/ping` every `heartbeat_interval_secs`
# (default 30) and flips the peer Healthy → Unhealthy after
# `heartbeat_failure_threshold` (default 3) consecutive failures.
# Unhealthy peers are still pickable as a last-resort fallback when no
# healthy peer satisfies the task's `required_caps`.
# heartbeat_interval_secs = 30
# heartbeat_failure_threshold = 3
# ── External MCP servers (optional) ───────────────────────────────────────────
# Phantom can spawn external Model-Context-Protocol (stdio) servers and surface
# their tools to every agent under a `<server_name>_<tool_name>` prefix. The
# child process is started on phantom-mesh startup and kept alive; it is killed
# when the daemon exits.
#
# Each `[[mcp_servers]]` block accepts:
# name — short tag used as the tool prefix (e.g. "secops_recon" →
# the server's `scan_target` tool is exposed as
# `secops_recon_scan_target` to the LLM)
# command — absolute path or PATH-resolvable binary to spawn
# args — argv list passed to the child
# env — extra environment variables (merged onto inherited env)
#
# Tools added this way are appended to every agent's effective tool list, so
# you do NOT have to enumerate them under `[agent.X].tools = [...]` (though
# you can mention them in `instructions` to nudge the model).
#
# ── Example: wire in phantom-secops's three MCP servers (red-team recon, ───
# ── blue-team log scanner, and config self-audit) ────────────────
# Prereq: clone https://github.com/marklight-dev/phantom-secops, then either
# (a) run `make mesh-mcp-config` over there and paste its output here, or
# (b) uncomment the blocks below and `export PHANTOM_SECOPS_ROOT=/abs/path`
# before starting phantom (the `cwd` field is silently ignored by
# phantom today, so the child inherits this process's CWD — set
# PYTHONPATH explicitly to make imports resolve).
#
# After the daemon starts, verify with the line REPL:
# phantom repl -c "/tools" # shows external (mcp) section
# phantom repl -c "/mcp test secops_recon" # re-pings the server
#
# Run a one-shot from the master agent:
# phantom exec --agent master "use secops_recon_scan_target to probe 127.0.0.1"
#
# [[mcp_servers]]
# name = "secops_recon"
# command = "python3"
# args = ["-m", "phantom_secops.mcp.secops_recon_server"]
# env = { PYTHONPATH = "/abs/path/to/phantom-secops" }
#
# [[mcp_servers]]
# name = "secops_log"
# command = "python3"
# args = ["-m", "phantom_secops.mcp.secops_log_server"]
# env = { PYTHONPATH = "/abs/path/to/phantom-secops" }
#
# [[mcp_servers]]
# name = "secops_self_audit"
# command = "python3"
# args = ["-m", "phantom_secops.mcp.secops_self_audit_server"]
# env = { PYTHONPATH = "/abs/path/to/phantom-secops" }
# ── Telegram Remote Control (optional) ───────────────────────────────────────
# Turns Telegram into a remote for this cluster (BIG-GOAL §P3 / OpenClaw
# epic E004): every Telegram message becomes a cluster command and the
# bot's reply is the agent's response to the cluster doing the work — not
# a standalone chatbot. Typically enabled only on the always-on cloud node.
#
# Setup:
# 1. Create a bot via @BotFather: https://t.me/BotFather
# 2. Export the token: export TELEGRAM_BOT_TOKEN=<token>
# 3. Find your Telegram user ID via @userinfobot
# 4. Uncomment and fill in below
#
# [telegram]
# bot_token_env = "TELEGRAM_BOT_TOKEN" # env var holding the bot token (required)
# allowed_users = [123456789] # your Telegram user ID; empty = allow all
# agent = "master" # which agent handles incoming messages