Configuration¶
Upgrading from pre-1.3.14? Read Upgrading from pre-1.3.14 before restarting the bot — the security hardening changed a few defaults and your admin/Telegram access may stop working until you complete the migration.
Runtime config file: ~/.sygen/config/config.json.
Seed source: <repo>/config.example.json (source checkout) or packaged fallback sygen_bot/_config_example.json (installed mode).
Config Creation¶
Primary path: sygen onboarding (interactive wizard) writes config.json with user-provided values merged into AgentConfig defaults.
Load & Merge Behavior¶
Config is merged in two places:
sygen_bot/__main__.py::load_config()- creates config on first start (copy from
config.example.jsonor Pydantic defaults), - deep-merges runtime file with
AgentConfigdefaults, - writes back only when new keys were added.
sygen_bot/workspace/init.py::_smart_merge_config()- shallow merge
{**defaults, **existing}withconfig.example.json, - preserves existing user top-level keys,
- fills missing top-level keys from
config.example.json.
Normalization detail:
- onboarding and runtime config load normalize
gemini_api_keydefault to string"null"in persisted JSON for backward compatibility. AgentConfigvalidator converts null-like text ("","null","none") toNoneat runtime.
Runtime edits persisted through config helpers include /model changes (model/provider/reasoning), webhook token auto-generation, and API token auto-generation.
API config persistence note:
load_config()intentionally does not auto-add theapiblock during default deep-merge (beta gating).sygen api enablewrites theapiblock (including generated token) intoconfig.json.
External API Secrets (~/.sygen/.env)¶
User-defined environment secrets for external APIs (e.g. PPLX_API_KEY, DEEPSEEK_API_KEY).
Standard dotenv syntax:
Propagation:
- host CLI execution: merged into subprocess env via
_build_subprocess_env() - Docker exec: injected as
-eflags viadocker_wrap() - Docker container creation: injected as
-eflags via_start_container() - sub-agents and background tasks: inherited through the same execution paths
Priority (highest to lowest):
- existing host environment variables (never overridden)
- provider-specific config (e.g.
gemini_api_keyinconfig.json) .envvalues (fill gaps only)
Changes take effect on the next CLI invocation (mtime-based cache invalidation, no restart needed).
AgentConfig (sygen_bot/config.py)¶
| Field | Type | Default | Notes |
|---|---|---|---|
log_level | str | "INFO" | Applied at startup unless CLI --verbose is used |
provider | str | "claude" | Default provider |
model | str | "opus" | Default model ID |
sygen_home | str | "~/.sygen" | Runtime home root |
idle_timeout_minutes | int | 1440 | Session freshness idle timeout (0 disables idle expiry) |
session_age_warning_hours | int | 12 | Adds /new reminder after threshold (every 10 messages) |
daily_reset_hour | int | 4 | Daily reset boundary hour in user_timezone |
daily_reset_enabled | bool | false | Enables daily session reset checks |
user_timezone | str | "" | IANA timezone used by cron/heartbeat/cleanup/session reset |
max_budget_usd | float \| None | None | Passed to Claude CLI |
max_turns | int \| None | None | Passed to Claude CLI |
max_session_messages | int \| None | None | Session rollover limit |
permission_mode | str | "bypassPermissions" | Default CLI --permission-mode. Used as fallback when a role has no entry in permission_mode_by_role. Valid values: acceptEdits, auto, bypassPermissions, default, dontAsk, plan. |
permission_mode_by_role | dict[str, str] | {"admin": "bypassPermissions", "operator": "acceptEdits", "viewer": "default"} | Per-role CLI permission mode. See Per-role permission modes below. |
cli_timeout | float | 1800.0 | Legacy/global timeout. Still used by cron/webhook cron_task, inter-agent turns, stale-process heartbeat cleanup, and as fallback for unknown timeout paths |
reasoning_effort | str | "medium" | Default Codex reasoning level |
file_access | str | "all" | File access scope (all, home, workspace) for file sends and API GET /files; unknown values fall back to workspace-only |
gemini_api_key | str \| None | None | Config fallback key injected for Gemini API-key mode |
transport | str | "telegram" | Messaging transport: "telegram" or "matrix" |
transports | list[str] | [] | List of transports to run in parallel (e.g. ["telegram", "matrix"]). When empty, falls back to single transport value. |
telegram_token | str | "" | Telegram bot token (required when transport=telegram) |
allowed_user_ids | list[int] | [] | Telegram user allowlist (applies in both private and group chats) |
allowed_group_ids | list[int] | [] | Telegram group allowlist (which groups the bot can operate in; default [] = no groups, fail-closed). In groups, both the group and the user must be allowlisted |
group_mention_only | bool | false | Mention/reply gating in group rooms. Telegram: filter only (no auth bypass). Matrix: in non-DM rooms this bypasses allowed_users and uses room + mention/reply as gate |
matrix | MatrixConfig | see below | Matrix homeserver connection (required when transport=matrix) |
streaming | StreamingConfig | see below | Streaming tuning |
docker | DockerConfig | see below | Docker sidecar config |
heartbeat | HeartbeatConfig | see below | Background heartbeat config |
cleanup | CleanupConfig | see below | Daily file-retention cleanup |
memory | MemoryConfig | see below | Memory system settings (injection, maintenance) |
webhooks | WebhookConfig | see below | Webhook HTTP server config |
fileshare | FileshareConfig | see below | Built-in fileshare HTTP server config |
api | ApiConfig | see below | Direct WebSocket API server config |
cli_parameters | CLIParametersConfig | see below | Provider-specific extra CLI flags |
image | ImageConfig | see below | Incoming image processing settings |
timeouts | TimeoutConfig | see below | Path-specific timeout policy (normal, background, subagent) |
tasks | TasksConfig | see below | Delegated background task system (TaskHub) |
scene | SceneConfig | see below | Scene indicators and technical footer |
transcription | TranscriptionConfig | see below | Audio/video transcription settings (language, model, custom command) |
update_check | bool | true | Enables periodic update observer (UpdateObserver). Verifies actual installed version via subprocess before notifying — will not offer upgrades for versions already installed externally. |
topic_defaults | dict[str, dict[str, str]] | {} | Per-topic default models. Keys are topic IDs (as strings), values are {"model": "<model_id>"}. Set via /topicmodel command. Resolution priority: @directive > topic_default > config.model |
additional_directories | list[str] | [] | Extra directories passed as --add-dir to Claude CLI, extending the sandbox beyond the workspace. Only absolute paths to existing directories are accepted. |
interagent_port | int | 8799 | Port for internal localhost API (InternalAgentAPI) |
interagent | InteragentConfig | see below | Inter-agent communication timeouts |
Multi-transport behavior¶
When transports is empty (default), the single transport value is used. When transports contains multiple entries (e.g. ["telegram", "matrix"]), MultiBotAdapter starts all listed transports in parallel and transport is auto-set to the first entry. A model validator normalizes both fields at load time so they stay consistent.
Per-role permission modes¶
The permission_mode_by_role field maps user roles to CLI --permission-mode values. When a CLI session is started on behalf of a user, the resolver looks the user's role up in this map and passes the corresponding mode to the Claude CLI. System-initiated calls (cron, webhooks, background observers) run with the "admin" role.
Resolution order:
- If the caller provides a role and the role is present in
permission_mode_by_role, use the mapped value. - Otherwise, fall back to the top-level
permission_modefield (legacy behaviour).
Recommended defaults:
| Role | Mode | Effect |
|---|---|---|
admin | bypassPermissions | No prompts — full access (unchanged legacy behaviour). |
operator | acceptEdits | File edits auto-allowed; tool calls that can modify shared state (e.g. arbitrary shell commands) still ask for confirmation. |
viewer | default | Prompts for every write and tool invocation. |
Valid values are the same as the Claude CLI's --permission-mode flag: acceptEdits, auto, bypassPermissions, default, dontAsk, plan. Unknown values fail config validation at load time.
To opt out of per-role resolution entirely (legacy behaviour), set permission_mode_by_role to {} — the resolver then always returns the top-level permission_mode.
Role sources (who resolves the role)¶
The role threaded into permission_mode_by_role can come from four places:
| Entry point | Source | Fallback when missing |
|---|---|---|
| REST / Admin WS | role claim in the JWT access token | global permission_mode |
| Telegram | message.from_user.id mapped via telegram_user_ids in users.json | global permission_mode |
Inter-agent (cron, webhooks, ask_agent*) | user_role="admin" or the SYGEN_CALLER_USER_ID env var that the framework injects from the authenticated transport | global permission_mode |
| None of the above | None | global permission_mode |
Per-user access control (users.json)¶
~/.sygen/_secrets/users.json stores user accounts used by the admin panel, the Telegram role resolver, and the inter-agent ACL. Each user record supports the following fields relevant to authorization:
| Field | Type | Notes |
|---|---|---|
username | str | Primary key; used as caller_user_id for inter-agent calls. |
role | str | One of admin, operator, viewer. Maps to permission_mode_by_role. |
active | bool | Inactive users are ignored by all role resolvers. |
allowed_agents | list[str] | Agents this user may reach via the inter-agent bus. Empty list or ["*"] = no restriction. Otherwise the recipient must appear in the list. Enforced in addition to the per-agent can_call allow list. |
telegram_user_ids | list[int] | Optional. Telegram from_user.id values that identify this user. When a Telegram message arrives from one of these ids, the user's role is used to pick the CLI permission mode. |
Unknown users (no matching record) fall through to legacy behaviour so rolling deployments do not break in-flight sessions.
Example:
{
"users": [
{
"username": "alice",
"role": "operator",
"active": true,
"allowed_agents": ["main", "research"],
"telegram_user_ids": [1234567]
}
]
}
MatrixConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
homeserver | str | "" | Matrix homeserver URL (e.g. https://matrix.org) |
user_id | str | "" | Bot user ID (e.g. @sygen:matrix.org) |
password | str | "" | Password for initial login |
access_token | str | "" | Optional manual restore source; runtime normally persists credentials in the Matrix store |
device_id | str | "" | Optional manual restore source paired with access_token |
allowed_rooms | list[str] | [] | Room IDs or aliases the bot may operate in |
allowed_users | list[str] | [] | Matrix user IDs allowed to interact |
store_path | str | "matrix_store" | E2EE key store directory, relative to sygen_home |
Notes:
- first successful login persists credentials to
~/.sygen/<store_path>/credentials.json(mode0o600), not back intoconfig.json - when
access_tokenanddevice_idare explicitly present inconfig.json, runtime restores from them and also mirrors them into the credentials store - The bot supports end-to-end encrypted rooms via
matrix-nio[e2e]. allowed_roomsandallowed_userstogether form the Matrix auth model.
CLIParametersConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
claude | list[str] | [] | Extra args appended to Claude CLI command |
codex | list[str] | [] | Extra args appended to Codex CLI command |
gemini | list[str] | [] | Extra args appended to Gemini CLI command |
Used by CLIServiceConfig for main-chat calls.
Argument shape note:
- each list element is passed as one CLI argument; do not combine multiple shell flags into one string such as
"--verbose --chrome"
Automation note:
- cron/webhook
cron_taskruns use task-levelcli_parametersfromcron_jobs.json/webhooks.json(no merge with globalcli_parameters).
TimeoutConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
normal | float | 600.0 | Default timeout for foreground chat turns (normal / normal_streaming) |
background | float | 1800.0 | Timeout for named background sessions (BackgroundObserver) |
subagent | float | 3600.0 | Reserved timeout bucket for sub-agent-specific paths |
warning_intervals | list[float] | [60.0, 10.0] | Warning thresholds for TimeoutController |
extend_on_activity | bool | true | Enables deadline extension when subprocess output is active |
activity_extension | float | 120.0 | Seconds added per granted extension |
max_extensions | int | 3 | Maximum activity-based extensions |
Runtime sync behavior:
AgentConfigkeeps backward compatibility withcli_timeout.- If
cli_timeout != 600.0andtimeouts.normalis still default, runtime validation copiescli_timeoutintotimeouts.normal. - If
timeouts.normalis explicitly set, it wins overcli_timeout.
Current execution-path usage:
- foreground chat turns:
resolve_timeout(config, "normal")->timeouts.normal - named background sessions (
/session):timeouts.background - delegated background tasks (
TaskHub):tasks.timeout_seconds - cron + webhook
cron_task: stillconfig.cli_timeout - inter-agent turns: still
config.cli_timeout - stale-process cleanup threshold:
config.cli_timeout * 2
Implementation status note:
cli/timeout_controller.pyand warning/extension config are implemented and tested.- provider wrappers and executor support
TimeoutControllerin production paths. - normal/streaming/named-session/heartbeat flows create controllers via
flows._make_timeout_controller(...). - timeout warning/extension callbacks are not yet wired to Telegram/API system-status output, so user-visible timeout status labels are not emitted by default.
TasksConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | Enables shared delegated task system (TaskHub) |
max_parallel | int | 5 | Max concurrent running tasks per chat in TaskHub |
timeout_seconds | float | 3600.0 | Timeout per delegated task run |
Task-Level Automation Overrides¶
Stored outside config.json in:
~/.sygen/cron_jobs.json(CronJob)~/.sygen/webhooks.json(WebhookEntry,cron_taskmode)
Common per-task fields:
- execution:
provider,model,reasoning_effort,cli_parameters - scheduling guards:
quiet_start,quiet_end,dependency
Cron-only field:
timezone(per-job timezone override)
Behavior notes:
- missing execution fields fall back to global config via
resolve_cli_config(), dependencyis global across cron + webhookcron_taskruns (sharedDependencyQueue),- quiet-hour checks run only when per-task quiet fields are set (no fallback to global heartbeat quiet settings).
StreamingConfig¶
| Field | Type | Default |
|---|---|---|
enabled | bool | true |
min_chars | int | 200 |
max_chars | int | 4000 |
idle_ms | int | 800 |
edit_interval_seconds | float | 2.0 |
max_edit_failures | int | 3 |
append_mode | bool | false |
sentence_break | bool | true |
DockerConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | false | Master toggle |
image_name | str | "sygen-sandbox" | Docker image name |
container_name | str | "sygen-sandbox" | Docker container name |
auto_build | bool | true | Build image automatically when missing |
mount_host_cache | bool | false | Mount host ~/.cache into container (see below) |
mounts | list[str] | [] | Extra host directories mounted into sandbox (/mnt/...) |
extras | list[str] | [] | Optional AI/ML package IDs to install in the Docker image (see below) |
Orchestrator.create() calls DockerManager.setup() when enabled. If setup fails, sygen logs warning and falls back to host execution.
mount_host_cache¶
Mounts the host's platform-specific cache directory into the container at /home/node/.cache:
| Platform | Host path |
|---|---|
| Linux | ~/.cache (or $XDG_CACHE_HOME) |
| macOS | ~/Library/Caches |
| Windows | %LOCALAPPDATA% |
Use case: browser-based skills (e.g. google-ai-mode) that use patchright/playwright need access to persistent browser profiles and browser binaries stored in the host cache. Without this, each container start requires a fresh CAPTCHA solve and Chrome download.
Disabled by default because it exposes the host cache directory to the sandbox.
mounts¶
User-defined directory mounts for project/data access inside Docker sandbox.
- each entry is expanded (
~, env vars), resolved, and validated as an existing directory - each entry is just a host directory path (for example
"/home/you/projects"), not Dockerhost:container[:mode]syntax - invalid or missing entries are skipped with warnings
- container target path is derived from host basename:
/mnt/<sanitized-name> - duplicate target names are disambiguated as
/mnt/name_2,/mnt/name_3, ...
Runtime note:
- updates are typically managed via
sygen docker mount|unmount - changing mounts requires bot restart (or
sygen docker rebuild) to affect container run flags
extras¶
Optional AI/ML packages installed into the Docker sandbox image at build time. Each entry is an ID from the extras registry (sygen_bot/infra/docker_extras.py).
Available extras:
| ID | Name | Category | Size |
|---|---|---|---|
ffmpeg | FFmpeg | Audio / Speech | ~100 MB |
whisper | Faster Whisper | Audio / Speech | ~500 MB |
opencv | OpenCV | Vision / OCR | ~100 MB |
tesseract | Tesseract OCR | Vision / OCR | ~40 MB |
easyocr | EasyOCR | Vision / OCR | ~2.5 GB |
pymupdf | PyMuPDF | Document Processing | ~50 MB |
pandoc | Pandoc | Document Processing | ~80 MB |
scipy | SciPy | Scientific / Data | ~130 MB |
pandas | pandas | Scientific / Data | ~60 MB |
matplotlib | Matplotlib | Scientific / Data | ~60 MB |
pytorch-cpu | PyTorch (CPU) | ML Frameworks | ~800 MB |
transformers | HF Transformers | ML Frameworks | ~2 GB |
playwright | Playwright | Web / Browser | ~450 MB |
Dependency resolution:
whisperdepends onffmpegeasyocrandtransformersdepend onpytorch-cpu- dependencies are auto-resolved at build time
Managed via sygen docker extras-add|extras-remove or during onboarding wizard. Changes require sygen docker rebuild to take effect.
When extras are configured, the supervisor startup timeout is dynamically extended to accommodate longer Docker build times.
HeartbeatConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | false | Master toggle |
interval_minutes | int | 30 | Loop interval |
cooldown_minutes | int | 5 | Skip if user active recently |
quiet_start | int | 21 | Quiet start hour in user_timezone |
quiet_end | int | 8 | Quiet end hour in user_timezone |
prompt | str | default prompt | Multiline default prompt references MAINMEMORY.md and cron_tasks/ |
ack_token | str | "HEARTBEAT_OK" | Suppression token |
group_targets | list[HeartbeatTarget] | [] | Per-group/topic heartbeat targets with optional overrides |
HeartbeatTarget¶
Each entry in group_targets identifies a specific group chat (and optional topic) to send heartbeat checks to. All optional fields override the global HeartbeatConfig when set; unset fields fall back to global values.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
chat_id | int | yes | Target group chat ID | |
topic_id | int \| None | no | None | Optional forum topic ID within the group |
prompt | str \| None | no | None | Per-target prompt override (falls back to global prompt) |
ack_token | str \| None | no | None | Per-target suppression token (falls back to global ack_token) |
interval_minutes | int \| None | no | None | Per-target interval override (falls back to global interval_minutes) |
quiet_start | int \| None | no | None | Per-target quiet-hour start (falls back to global quiet_start) |
quiet_end | int \| None | no | None | Per-target quiet-hour end (falls back to global quiet_end) |
ImageConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
max_dimension | int | 3000 | Maximum width/height in pixels; images exceeding this are resized proportionally |
output_format | str | "webp" | Target image format (e.g. webp, jpeg, png) |
quality | int | 85 | Compression quality for lossy formats (WebP, JPEG) |
Applied to incoming images across all transports (Telegram, Matrix, API). See files/image_processor.py for implementation details.
TranscriptionConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
language | str | "auto" | Language code ("en", "ru", "de", etc.). "auto" = auto-detect |
model | str | "small" | Whisper model size: "tiny", "base", "small", "medium", "large" |
command | str \| null | null | Custom external command (see below) |
Controls audio/video transcription behavior for voice messages and video notes.
Strategy chain¶
Transcription tries backends in this order:
- Custom command (only when
commandis set) — receives audio file path as argument, prints transcript to stdout - OpenAI Whisper API — requires
OPENAI_API_KEYin environment or.env - Local
whisperCLI — Pythonopenai-whisperpackage whisper-cli— whisper.cpp (C++ implementation, 5-10x faster than Python on CPU)
First successful backend wins. If all fail, an error with installation hints is returned.
Custom command interface¶
When command is set, it is called as:
Environment variables passed to the command:
TRANSCRIPTION_LANGUAGE— configured language (e.g."auto","en")TRANSCRIPTION_MODEL— configured model size (e.g."small")
The command must print the transcript text to stdout and exit with code 0. On non-zero exit or empty output, the next strategy in the chain is tried.
Model sizes¶
| Model | Size | Quality | Speed (CPU) |
|---|---|---|---|
tiny | ~75 MB | Basic | Very fast |
base | ~150 MB | Good | Fast |
small | ~500 MB | Great | Medium |
medium | ~1.5 GB | Excellent | Slow |
large | ~3 GB | Best | Very slow |
Quick setup¶
Fastest path for new users — cloud API (no local install):
Fastest local path — whisper.cpp:
# Install whisper.cpp, then download model:
# https://github.com/ggerganov/whisper.cpp
whisper-cli --help # verify installation
Hot-reloadable: yes (changes apply without restart).
SceneConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
reaction_style | str | "seen" | Emoji reactions: "off" = none, "seen" = 👀 on receipt + 👌 on done, "detailed" = 👀→🤔→✍️→💯→👌 real-time status |
technical_footer | bool | false | Appends model/token/cost/time footer to agent responses |
CleanupConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | Master toggle |
media_files_days | int | 30 | Retention for media files (telegram + matrix) |
output_to_user_days | int | 30 | Retention in workspace/output_to_user/ |
api_files_days | int | 30 | Retention in workspace/api_files/ |
check_hour | int | 3 | Local hour in user_timezone for cleanup run |
Cleanup implementation detail:
- cleanup is recursive (
_delete_old_fileswalks nested files viarglob("*")), - after file deletion, empty subdirectories are pruned,
- dated upload folders (
.../YYYY-MM-DD/...) are cleaned when contained files exceed retention and directories become empty.
MemoryConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | true | Master toggle for mechanical memory maintenance |
module_line_limit | int | 80 | Max lines per memory module (agent gets size warnings above this) |
session_max_age_days | int | 30 | Session cleanup threshold |
check_hour | int | 4 | Hour for memory maintenance (4 AM) |
hook_compact_lines | int | 20 | Max lines per module injected in periodic memory hook |
inject_all_modules | bool | false | Inject ALL modules (not just Always Load) into agent context |
vector_search | bool | false | Enable semantic vector search over memory facts |
vector_model | str | "" | Embedding model name; empty = auto-detect best available |
vector_results | int | 5 | Max facts returned per vector search query |
Memory injection behavior¶
Memory modules are injected into the agent's context at two points:
- Session start — full content of selected modules is injected via
--append-system-prompt - Every 6 messages —
MAINMEMORY_REMINDERhook re-injects module content (truncated tohook_compact_linesper module) to prevent knowledge loss after context compaction
Which modules are injected depends on inject_all_modules:
false(default) — only modules listed in the "Always Load" table inMAINMEMORY.md(typicallyuser.mdanddecisions.md)true— all.mdfiles inmemory_system/modules/are injected
Token cost estimates¶
With 2 Always Load modules (~25 lines each): - Session start: ~1.5K tokens (one-time) - Hook (every 6 msgs): ~600 tokens with hook_compact_lines=20
With 5 modules and inject_all_modules=true: - Session start: ~3K tokens (one-time) - Hook (every 6 msgs): ~1.2K tokens with hook_compact_lines=20
For users with high-tier API plans who prioritize agent knowledge over token cost, set inject_all_modules=true and increase hook_compact_lines (e.g. 50 or 100).
Vector search (semantic memory)¶
Optional feature for semantic search over memory facts. Instead of injecting entire modules, the hook searches for facts relevant to the current conversation.
Installation:
Enable:
How it works:
- Memory module facts (list items) are automatically indexed as embeddings
- On each hook trigger, the user's message is used as a search query
- Top-N semantically relevant facts are injected instead of full modules
- Falls back to module-based injection if vector search returns no results
Model priority:
sentence-transformersmultilingual model (if installed) — best for non-English- ChromaDB built-in ONNX model — English-focused, no extra dependencies
- Custom model via
vector_modelconfig option
Vector search complements (not replaces) the module-based system. Both can be active simultaneously — vector search provides targeted facts while Always Load ensures core context.
WebhookConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | false | Master toggle |
host | str | "127.0.0.1" | Bind address (localhost by default) |
port | int | 8742 | HTTP server port |
token | str | "" | Global bearer fallback token (auto-generated when webhooks start) |
max_body_bytes | int | 262144 | Max request body size |
rate_limit_per_minute | int | 30 | Sliding-window rate limit |
FileshareConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | false | Master toggle |
host | str | "127.0.0.1" | Bind address |
port | int | 8090 | HTTP server port |
When enabled, sets FILESHARE_URL and FILESHARE_DOWNLOADS environment variables for agent tools (used by send_large_file.py). See fileshare.md for full details.
ApiConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | bool | false | Master toggle |
host | str | "0.0.0.0" | Bind address |
port | int | 8741 | API HTTP/WebSocket port |
token | str | "" | Bearer/WebSocket auth token (generated by sygen api enable, with runtime generation fallback on API start) |
chat_id | int | 0 | Default API session chat (0 means fallback to first allowed_user_ids entry, else 1) |
allow_public | bool | false | Suppresses Tailscale-not-detected warning |
Runtime note (Orchestrator._start_api_server + ApiServer._authenticate):
config.api.chat_idis used via truthiness (0falls back),- fallback default comes from first
allowed_user_idsentry (fallback1), - per-connection auth payload may override via:
{"type":"auth","chat_id":...}(positive int),- optional
channel_id(positive int) for per-channel session isolation (SessionKey.topic_id), - clients can override only for that connection; persisted default stays in config.
InteragentConfig¶
| Field | Type | Default | Notes |
|---|---|---|---|
sync_timeout | float | 600.0 | Timeout (seconds) for synchronous inter-agent requests via bus.send() |
async_timeout | float | 3600.0 | Timeout (seconds) for asynchronous inter-agent tasks via bus.send_async() |
Configurable via config.json:
Runtime hot-reload (config_reload.py)¶
Orchestrator.create() starts ConfigReloader, which polls config.json every 5 seconds, validates it with AgentConfig, diffs top-level fields, and applies safe changes without restart.
Hot-reloadable top-level fields:
model,provider,reasoning_effortcli_timeout,max_budget_usd,max_turns,max_session_messagesidle_timeout_minutes,session_age_warning_hours,daily_reset_hour,daily_reset_enabledpermission_mode,permission_mode_by_role,file_access,user_timezonestreaming,heartbeat,cleanup,cli_parametersallowed_user_ids,allowed_group_ids,group_mention_onlytranscriptiontimeoutsis currently restart-required (not in hot-reloadable set)
Observer lifecycle caveat:
- heartbeat/cleanup values hot-reload into config
- observer start/stop is not hot-toggled
- enabling heartbeat/cleanup after startup requires restart if the observer was not started initially
Restart-required top-level fields:
transport,telegram_token,matrixdocker,api,webhookssygen_home,log_level,gemini_api_key,timeouts,tasks
Restart classification is computed from AgentConfig top-level schema fields.
Model Resolution¶
ModelRegistry (sygen_bot/config.py):
- Claude models are hardcoded:
haiku,sonnet,opus. - Gemini aliases are hardcoded:
auto,pro,flash,flash-lite. - Runtime Gemini models are discovered from local Gemini CLI files at startup.
- Provider resolution (
provider_for(model_id)): - Claude when in
CLAUDE_MODELS, - Gemini when in aliases/discovered set or when model looks like
gemini-*/auto-gemini-*, - otherwise Codex.
Timezone Resolution¶
resolve_user_timezone(configured) in sygen_bot/config.py:
- valid configured IANA timezone,
$TZenv var,- host system detection:
- Windows: local datetime tzinfo,
- POSIX:
/etc/localtimesymlink, - fallback
UTC.
Returns ZoneInfo when available, otherwise a UTC tzinfo fallback object with key="UTC" on systems without timezone data. Used by cron scheduling, session daily-reset checks, heartbeat quiet hours, and cleanup scheduling.
reasoning_effort¶
UI values: low, medium, high, xhigh.
Main-chat flow:
AgentConfig -> CLIServiceConfig -> CLIConfig -> CodexCLI (-c model_reasoning_effort=<value> when relevant).
Automation flow:
resolve_cli_config()applies reasoning effort only for Codex models that support the requested effort.
Codex Model Cache¶
Path: ~/.sygen/config/codex_models.json.
Behavior:
- loaded at orchestrator startup (
CodexCacheObserver.start()), - startup load is forced refresh (
force_refresh=True), - checked hourly in background,
load_or_refresh()uses cache if<24hold, otherwise re-discovers via Codex app server,- consumed by
/modelwizard,resolve_cli_config()for cron/webhook validation, and/diagnoseoutput.
Gemini Model Cache¶
Path: ~/.sygen/config/gemini_models.json.
Behavior:
- loaded at orchestrator startup (
GeminiCacheObserver.start()), - startup load uses cached data when fresh and refreshes only when stale/missing,
- refreshed hourly in background,
- refresh callback updates runtime Gemini model registry (
set_gemini_models(...)) used by directives and model selector.
agents.json (Multi-Agent Registry)¶
Path: ~/.sygen/agents.json.
Top-level JSON array of SubAgentConfig objects. Each entry defines a sub-agent that runs alongside the main agent.
Managed via:
sygen agents add <name>(interactive CLI, currently Telegram-focused)sygen agents remove <name>(CLI)create_agent.py/remove_agent.pytool scripts (from within a CLI session)- manual file editing (auto-detected by
FileWatcher)
SubAgentConfig fields¶
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
name | str | yes | Unique lowercase identifier | |
transport | str | no | "telegram" | "telegram" or "matrix" |
telegram_token | str | conditional | Required when transport=telegram | |
matrix | MatrixConfig | conditional | Required when transport=matrix | |
allowed_user_ids | list[int] | no | [] | Telegram user allowlist |
allowed_group_ids | list[int] | no | [] | Telegram group allowlist |
group_mention_only | bool | no | inherited | Mention/reply gating toggle (transport-specific behavior) |
provider | str | no | inherited | Default provider |
model | str | no | inherited | Default model |
log_level | str | no | inherited | |
idle_timeout_minutes | int | no | inherited | |
session_age_warning_hours | int | no | inherited | |
daily_reset_hour | int | no | inherited | |
daily_reset_enabled | bool | no | inherited | |
max_budget_usd | float | no | inherited | |
max_turns | int | no | inherited | |
max_session_messages | int | no | inherited | |
permission_mode | str | no | inherited | |
cli_timeout | float | no | inherited | |
reasoning_effort | str | no | inherited | |
file_access | str | no | inherited | |
streaming | StreamingConfig | no | inherited | |
docker | DockerConfig | no | inherited | |
heartbeat | HeartbeatConfig | no | inherited | |
cleanup | CleanupConfig | no | inherited | |
webhooks | WebhookConfig | no | inherited | |
api | ApiConfig | no | disabled | Disabled by default for sub-agents |
cli_parameters | CLIParametersConfig | no | inherited | |
user_timezone | str | no | inherited | |
additional_directories | list[str] | no | [] (workspace-only) | Does not inherit from main. Admin grants per-agent paths (see Sub-agent sandbox). |
"inherited" means the value comes from the main agent's config.json when omitted.
Timeout nuance:
SubAgentConfigcurrently has no dedicatedtimeoutsfield.SubAgentConfigcurrently has no dedicatedtasksfield.- sub-agents inherit the main agent
timeoutsblock through merge base. - sub-agents inherit the main agent
tasksblock through merge base.
Example:
[
{
"name": "researcher",
"telegram_token": "123456:ABC...",
"allowed_user_ids": [12345678],
"provider": "claude",
"model": "sonnet"
},
{
"name": "coder",
"transport": "matrix",
"matrix": {
"homeserver": "https://matrix.example.com",
"user_id": "@coder:example.com",
"password": "...",
"allowed_rooms": ["!room:example.com"],
"allowed_users": ["@user:example.com"]
},
"provider": "codex",
"reasoning_effort": "high"
}
]
Sub-agent runtime merge behavior¶
merge_sub_agent_config(main, sub, agent_home) builds the effective sub-agent AgentConfig with this priority:
- main agent config (
config.json) as base - sub-agent's own
config/config.json(if present) - explicit non-null overrides from
agents.json(highest priority)
Then it always forces:
sygen_home = ~/.sygen/agents/<name>/transport,telegram_token,matrix,allowed_user_ids, andallowed_group_idsfrom the sub-agent entryapi.enabled = falsewhen no explicitapiblock is providedadditional_directories = []reset before layers 2 and 3 — sub-agents never inherit the main agent's extra paths (see Sub-agent sandbox)
Notes:
/modelchanges in a sub-agent chat are written back toagents.json, so restart/reload uses the updated values from that registry file
Sub-agent sandbox¶
Sub-agent filesystem access is enforced by two independent layers:
- Claude CLI
--add-dir— baseline sandbox when the CLI runs inpermission_mode="default"/"acceptEdits". The allowed roots arecwdplus the paths inadditional_directories. - PreToolUse hook (
~/.sygen/agents/<name>/workspace/.claude/ file_sandbox.py, since 1.3.45) — blocksRead,Edit,Write,MultiEdit,NotebookEdit,Grep,Globcalls whose target is outside the sub-agent's workspace plusadditional_directories.Bashand other tools pass through so Gradle, Xcode, docker, and git keep working. The hook is required because admin-role sub-agents run withpermission_mode="bypassPermissions", which skips the--add-dircheck entirely — without the hook, a bypass-permissions sub-agent could read anywhere the user process can.
The hook reads additional_directories directly from ~/.sygen/_secrets/agents.json on every call, so changes from the admin UI take effect on the next CLI invocation with no redeploy. The agent identity comes from the SYGEN_AGENT_NAME env var injected by sygen_bot.cli.executor._build_subprocess_env, so cd into an allowlist directory cannot disable the sandbox.
_ensure_sandbox_hook in sygen_bot/workspace/init.py installs the hook on every init_workspace() call for any path of the form ~/.sygen/agents/<name>/workspace/. The main agent (~/.sygen/workspace/) is skipped — it runs as the admin user with full file access.
For the main agent, the CLI sandbox is sygen_home + config.additional_directories, which typically includes the Sygen source checkout and the admin panel.
Sub-agents are untrusted delegates: each gets its own ~/.sygen/agents/<name>/ workspace and nothing else.
- Default:
additional_directories = [](workspace-only). - Main's
additional_directoriesis not inherited by sub-agents. - Admin grants extra paths per sub-agent via:
- the admin panel (Agents → select agent → Sandbox), or
- editing the
additional_directoriesfield on the agent's entry in~/.sygen/_secrets/agents.json, or - the sub-agent's own
config/config.json(useful when the sub-agent runs on a different host that was provisioned separately). - Sub-agents cannot extend their own sandbox from inside a chat session. They can ask the main agent or the admin; granting is a privileged action.
REST API: PUT /api/agents/{name} with body {"additional_directories": ["/abs/path", ...]} (admin-only). Paths are validated (absolute, must exist as directories) and de-duplicated.
agents.json watcher behavior¶
AgentSupervisor watches agents.json (mtime poll every 5s):
- new entry -> start sub-agent
- removed entry -> stop sub-agent
- restart triggers for running agents:
transportchanged- Telegram identity changed (
telegram_token) - Matrix identity changed (
matrix.homeserverormatrix.user_id) - other field changes currently do not auto-restart running agents
For non-token field updates on a running agent, use /agent_restart <name> (or restart the bot) to apply them immediately.
Upgrading from pre-1.3.14¶
Version 1.3.14 landed a security hardening pass that changes defaults a pre-1.3.14 install was silently relying on. After pip install -U sygen (or /upgrade), walk through the steps below before restarting the bot. Skipping them typically manifests as: "Claude CLI approval popups for every file edit and shell command, but there is no popup UI in Telegram."
1. Secrets moved to ~/.sygen/_secrets/¶
Files that used to live directly in ~/.sygen/ now live in ~/.sygen/_secrets/ (mode 0700, files 0600):
users.jsonagents.jsonmain_agent_token.json
The framework reads both locations during the 1.3.x line — the legacy path keeps working. But all new writes go to _secrets/, so if you have tooling that edits ~/.sygen/users.json directly, point it at ~/.sygen/_secrets/users.json or use the admin API.
2. Telegram users need telegram_user_ids mapping¶
The CLI permission mode is now chosen from permission_mode_by_role:
"permission_mode_by_role": {
"admin": "bypassPermissions",
"operator": "acceptEdits",
"viewer": "default"
}
For a Telegram message, the bot looks up the sender's numeric from_user.id in every record's telegram_user_ids list in users.json. If no record matches, the role resolves to None and the CLI is started with permission_mode="default" — Claude will ask for approval on every write and tool call, and Telegram has no UI to answer those prompts, so the session hangs.
Fix: make sure every human who talks to the bot has a record in users.json whose telegram_user_ids contains their Telegram ID. The admin panel's Users page exposes this field (since admin 0.3.3); or edit ~/.sygen/_secrets/users.json directly.
Tip: send a message to the bot from a machine you can tail logs on — the bot logs the unknown sender's ID on the first message, which you can then paste into the admin form.
Fresh installs from 1.3.23+: the bootstrap admin's telegram_user_ids is seeded automatically from config.allowed_user_ids on first run, so the setup wizard's single "your Telegram ID" prompt now populates both ACL gates. Upgrades from earlier versions still need a one-time manual edit if the admin record was created before this behavior existed.
3. Inter-agent calls require caller_token¶
Each sub-agent's record in agents.json gained an interagent_token field. The Sygen CLI writes it automatically when a sub-agent is created after 1.3.14; for older sub-agents the framework still accepts token-less requests but logs a warning ("running in legacy mode"). You can regenerate the token by recreating the sub-agent, or edit _secrets/agents.json by hand and add a 32+ char random string as "interagent_token".
4. The --user-id flag is gone¶
ask_agent.py and ask_agent_async.py no longer accept --user-id — the caller identity is taken only from the SYGEN_CALLER_USER_ID env var, which the framework sets from the authenticated transport. Passing the flag now exits with code 2 and a message explaining the change. Cron scripts and workflow steps that pass --user-id must be edited. Rationale: the flag let a prompt-injected LLM tool call impersonate any user and bypass per-user ACLs.
5. Additional directories for the Claude sandbox¶
If Sygen now lives at a non-default path (e.g. you develop it outside ~/.sygen/), add it to cli.additional_directories so Claude can read files from the source checkout:
Validation rejects relative paths and non-existent directories at startup — you'll see a clear error rather than a silent bypass.
6. Sub-agents are now workspace-only by default¶
Starting with the current release, sub-agents no longer inherit the main agent's additional_directories. On upgrade, every existing sub-agent is silently narrowed to its own ~/.sygen/agents/<name>/ workspace. No config migration is required.
If an existing sub-agent genuinely needs access outside its workspace (for example, a "builder" agent that touches the Sygen source), grant the paths explicitly:
- Admin panel → Agents → open the agent → Sandbox → add the path and save
- Or edit
additional_directorieson the agent's entry in~/.sygen/_secrets/agents.json.
The main agent is unaffected — its sandbox continues to come from config.json and still covers the full set of admin-granted paths.