Skip to content

Latest commit

 

History

History
832 lines (715 loc) · 45.4 KB

File metadata and controls

832 lines (715 loc) · 45.4 KB

Configuration reference

Everything Bamboo reads from ${data_dir}/config.json (data_dir defaults to ${HOME}/.bamboo, override with BAMBOO_DATA_DIR or --data-dir), plus the handful of sibling files and environment variables that participate in configuration.

Prefer not to hand-edit JSON? bamboo init writes a starter config, bamboo config set <dotted.key> <value> changes one value at a time (secret keys are encrypted automatically — see Secrets below), and bamboo config [--show-secrets] prints the resolved config. This document is for when you need to know exactly what a key does or edit the file by hand.

Precedence: config.json < environment variables < CLI flags (bamboo serve --port ... wins over everything). Provider selection specifically is providers.<name> / provider_instances.<id> (file) → BAMBOO_PROVIDER / BAMBOO_<PROVIDER>_API_KEY (env, in-memory only, never persisted) → --provider (CLI).

Source of truth for every struct below: crates/infra/bamboo-config/src/config.rs (pub struct Config, around line 1069) unless noted otherwise. Struct field lists here are derived directly from that code — if the two disagree, the code wins; please file an issue.

Top-level shape

{
  "provider": "anthropic",
  "providers": { "anthropic": { "api_key": "sk-ant-...", "model": "claude-sonnet-4-6" } },
  "server": { "port": 9562, "bind": "127.0.0.1" }
}

Every top-level key is optional (#[serde(default)]) — a config with only provider/providers is valid; every field below silently falls back to its default. The full field list of Config:

Field Type Notes
http_proxy / https_proxy String Outbound proxy URLs for provider HTTP calls.
proxy_auth_credential_ref Option<String> Stable reference to isolated proxy credentials (normally proxy.default.auth); no proxy plaintext, ciphertext, or mask is stored in ordinary config.
provider String Default provider name. Default "anthropic".
defaults Option<DefaultsConfig> Per-role model routing (chat/fast/vision/planning/...); only consulted when features.provider_model_ref is on.
providers ProviderConfigs Legacy single-instance-per-type provider configs. See Providers.
provider_instances HashMap<String, ProviderInstanceConfig> Authoritative provider configs, keyed by a stable routing id (e.g. two Anthropic keys under different labels). Runtime construction, model resolution, model discovery and child-agent credential scoping read these entries directly.
default_provider_instance Option<String> Which enabled provider_instances entry is the default. A missing id is accepted only as a temporary hybrid reference to a real legacy provider stanza.
server ServerConfig HTTP bind/port/TLS. See Server.
keyword_masking KeywordMaskingConfig Outbound-body secret scrubbing. See Keyword masking.
anthropic_model_mapping / gemini_model_mapping { mappings: HashMap<String,String> } Alias an OpenAI-shaped model id (e.g. "gemini-pro") to the real upstream model id for that provider's compat endpoint.
hooks HooksConfig Request preflight hooks; today just image_fallback (text-only-model image handling).
tools ToolsConfig { disabled: Vec<String> } — tool names omitted from every session's schema globally.
skills SkillsConfig { disabled: Vec<String> } — skill ids excluded from selection/loading globally.
env_vars Vec<EnvVarEntry> User-managed env vars injected into Bash-tool child processes. secret: true entries persist only stable credential_ref/configured metadata; their values live in the isolated credential store and are returned masked by the API.
default_work_area Option<DefaultWorkAreaConfig> { path: Option<String> } — default workspace when a session has none set.
access_control Option<AccessControlConfig> Password gate for the HTTP API/UI (password_enabled, hashed+salted).
features FeatureFlags { provider_model_ref: bool, dynamic_model_routing: bool } — incremental rollout toggles, both off by default.
stream_timeout StreamTimeoutConfig Independent transport, first-semantic, and midstream-semantic watchdog deadlines. See below.
context_management ContextManagementConfig Selects legacy summary compression or the opt-in exact-history retrieval window. See below.
memory Option<MemoryConfig> Memory/auto-dream/gardener settings. See below.
subagents SubagentsConfig Sub-agent execution + the claude_code executor. See below.
cluster_fabric ClusterFabricConfig Operator-managed remote nodes for deploying broker-agent workers over SSH; empty by default. SSH secrets encrypted at rest.
mcp (on-disk key mcpServers) McpConfig External tool servers. See MCP servers.
notifications NotificationsConfig Desktop/ntfy/Bark delivery channels. See below.
connect ConnectConfig Not actually stored here — see connect.
plugin_trust PluginTrustConfig Plugin install trust policy. See below.
extra BTreeMap<String, Value> Catch-all flatten for keys not (yet) promoted to a typed field — permissions, externalAgents, subagentRouting, setup-wizard state, etc. live here. Round-trips losslessly even for fields this version of Bamboo doesn't know about.

Env variable writes use the dedicated revisioned /bamboo/env-vars API. Its revision is the env-domain CAS revision stored in the credential envelope; every semantic env change (including metadata, public values, ordering, and deletes) advances it once, while a true no-op keeps it and emits no change event. config.json participates in the same recoverable manifest transaction and is hash-CAS protected. Secret inputs use three states: omitted value keeps an existing secret, value: "" explicitly clears it, and a non-empty value replaces it. Masks and client-sent credential_ref/configured/value_encrypted fields are rejected. Existing Lotus builds do not yet send this revision or omit value for metadata-only edits; updating that client contract is deferred to the Lotus follow-up and is not part of Bamboo Issue #597.

Providers

provider_instances is the durable and runtime authority. The legacy single-instance provider / providers shape remains accepted as a serde/migration compatibility input, most importantly so older installations can be materialized into provider instances safely on cold start:

{
  "provider": "anthropic",
  "providers": {
    "anthropic": { "api_key": "sk-ant-...", "model": "claude-sonnet-4-6" }
  }
}

Each provider stanza (OpenAIConfig / AnthropicConfig / GeminiConfig / CopilotConfig / BodhiConfig, all in config.rs) shares this core shape — api_key (write-only; persisted as api_key_encrypted, never re-emitted plaintext by GET), base_url (override the upstream endpoint — self-hosted proxies, Azure-style deployments, etc.), model, fast_model, vision_model, reasoning_effort, responses_only_models: Vec<String> (force these models onto the OpenAI Responses API path), request_overrides (provider-specific per-endpoint HTTP header/body tweaks), and an extra flatten for forward-compat fields. AnthropicConfig adds max_tokens and thinking_replay_always (needed by some Anthropic-compatible upstreams, e.g. GLM's /anthropic endpoint). BodhiConfig adds target_provider (which of openai/anthropic/gemini the Bodhi proxy should present as). CopilotConfig has no api_key at all — it authenticates via a cached OAuth token (headless_auth for headless/CI login).

For GPT-5.6+ OpenAI Responses requests from the agent loop, Bamboo derives a stable, session-scoped prompt_cache_key as a domain-separated SHA-256 hash. The raw session identifier is never serialized into the provider request, and non-agent requests do not receive a generated key. The key is only a cache affinity hint that can improve routing to a matching prefix; it does not guarantee a cache hit. request_overrides body patches run afterward, so an operator may replace the generated key or remove prompt_cache_key entirely.

The server idempotently materializes a usable selected legacy provider into provider_instances when it opens the modular configuration. Built-in provider type names become stable instance ids (openai, anthropic, and so on), the selected id becomes default_provider_instance, and provider-specific fields such as Anthropic max_tokens / thinking_replay_always, Copilot headless_auth, and Bodhi target_provider are retained. Existing credential references are reused; plaintext is never copied into providers.json. Migration commits use the provider-section revision and the configuration facade's recoverable transaction protocol. Reopening an already migrated configuration is a byte/revision no-op. If the provider or credential authority is degraded, a legacy reconciliation is pending, or the exact base revision has changed, migration does not overwrite that state; startup retains the readable last-known-good/hybrid view and retries on a later safe open.

For one or more provider accounts, the canonical shape is:

{
  "default_provider_instance": "work",
  "provider_instances": {
    "work": { "provider_type": "anthropic", "api_key": "sk-ant-work-...", "model": "claude-sonnet-4-6" },
    "personal": { "provider_type": "anthropic", "api_key": "sk-ant-personal-...", "enabled": true }
  }
}

provider_instances entries have the same field set as the legacy stanzas plus provider_type (which of the five kinds this is) and enabled (default true). An explicit instance id always wins over a same-named legacy alias, including when that instance is disabled or invalid; Bamboo never silently resurrects the stale alias. A temporary hybrid may keep a legacy default id only when a real stanza with that id still exists. Other instance-native paths do not re-project instances into global legacy slots.

BAMBOO_PROVIDER may select an exact instance id. A built-in type value selects the lexicographically first enabled instance of that type, making multi-account startup deterministic. BAMBOO_OPENAI_API_KEY, BAMBOO_ANTHROPIC_API_KEY, and BAMBOO_GEMINI_API_KEY hydrate only instances marked for the standard environment override; they do not recreate legacy slots. Legacy-materialized instances keep that binding so the historical precedence remains stable: a present environment key wins, while a migrated credential reference remains the fallback after the variable is removed. The persisted marker records only the binding, never the secret. An environment-only instance with no stored fallback reports configured: false and source: "environment" whenever the variable is absent.

Use the revisioned GET/PUT /v1/bamboo/config/provider-settings contract or the provider-instance CRUD endpoints for settings, and POST /v1/bamboo/provider-catalog/fetch-models for live model discovery. The legacy GET/POST /v1/bamboo/settings/provider and POST /v1/bamboo/settings/provider/models routes are no longer registered; older clients receive 404 and must migrate to the canonical contracts. This HTTP retirement does not remove the on-disk compatibility input described above.

Server

{ "server": { "port": 9562, "bind": "127.0.0.1", "workers": 10 } }

port (default 9562), bind (default 127.0.0.1), static_dir (serve the bundled frontend from a custom path), workers (Actix worker threads, default 10), tls: Option<TlsConfig> (cert_file/key_file PEM paths for manual TLS termination — no ACME/auto-cert). All overridable per-invocation with bamboo serve --port/--bind/--workers.

Tools, skills, hooks

  • tools.disabled: Vec<String> — tool names (e.g. "Bash") hidden from every session's tool schema, globally. Compare to the SDK's per-agent AgentBuilder::tools([...]), which scopes selection to one in-process Agent instead.
  • skills.disabled: Vec<String> — skill ids excluded from selection/loading globally.
  • hooks.image_fallback — how image parts are handled when the effective model/path is text-only (drop, OCR-replace, etc. — see ImageFallbackHookConfig). ViewImage returns a base64 multimodal image by default; with this hook enabled in vision mode, Bamboo asks the configured vision model (or its configured fallback) for a textual description before the next model turn.
  • lifecycle_hooks — config-driven command or external .js/.py/.sh/ .ps1/.bat script handlers for session, prompt, tool, compaction, and notification events. It lives in hooks.json; see the lifecycle hooks guide for runtime selection, the security model, and the input/output contract.

LLM stream timeouts

{
  "stream_timeout": {
    "transport_idle_timeout_secs": 120,
    "first_semantic_timeout_secs": 600,
    "semantic_idle_timeout_secs": 600
  }
}

The three watchdogs measure different signals and apply identically to the main response stream and auxiliary silent model calls:

Field Default Meaning
transport_idle_timeout_secs 120 Maximum time for the provider call to establish its response stream, and the maximum subsequent gap between successfully received, non-empty response-body chunks. SSE ping/lifecycle events, comment heartbeats, and partial event fragments count even when they contain no token.
first_semantic_timeout_secs 600 Maximum time from request dispatch to the first text, reasoning, or tool-call delta. Transport keepalives do not extend it.
semantic_idle_timeout_secs 600 Maximum semantic-progress gap after output starts. Transport keepalives do not extend it.

Every value must be between 1 and 86400 seconds. Invalid persisted values are rejected by config loading; invalid values constructed by an embedding are replaced with the safe defaults. Timeout errors report the expired phase, deadline, provider/model identifiers, and last transport/semantic activity, but never include prompts or raw provider payloads. A stream timeout is not retried after text, reasoning, or tool-call output has started, because replay could duplicate externally visible state. A timeout before any semantic output on the primary response stream is marked retry-safe and may use the agent loop's existing bounded turn retry policy. Auxiliary model calls are bounded by the same watchdogs but never replay the containing agent turn.

OpenAI-compatible proxy heartbeats

An OpenAI-compatible proxy should establish the response stream within transport_idle_timeout_secs, then either forward upstream response bytes or emit an SSE heartbeat more frequently than that deadline. Bamboo treats any successfully received, non-empty body chunk as transport activity before parsing SSE, including the standard comment form : keep-alive\n\n. These internal activity markers do not become model output and do not extend either semantic deadline.

For example, CLIProxyAPI supports periodic streaming heartbeats with:

streaming:
  keepalive-seconds: 15

CLIProxyAPI documents 0 (disabled) as the default. Operators using that or another compatible proxy should choose a heartbeat interval safely below the transport timeout. If a proxy cannot emit heartbeats during long upstream reasoning gaps, configure transport_idle_timeout_secs at least as high as the intended semantic wait instead; disabling the bounded transport watchdog is not recommended.

Context management

context_management defaults to {"strategy":"summary"}. Missing configuration therefore keeps the existing model-generated conversation summary behavior. The alternative retrieval_window strategy keeps exact raw messages in the Session store, removes eligible older complete turns only from the active provider window, and directs the model to session_history_current when it needs earlier evidence.

{
  "context_management": {
    "strategy": "retrieval_window",
    "retrieval_window": {
      "min_recent_user_turns": 3,
      "trigger_usage_ratio": 0.8,
      "target_usage_ratio": 0.6,
      "history_tool_required": true,
      "fallback_strategy": "none"
    }
  }
}
Field Default What it does
strategy summary summary preserves the current summarizer path; retrieval_window enables exact-history archival at the ordinary pre-turn pressure boundary.
retrieval_window.min_recent_user_turns 3 Minimum newest complete user-anchored turns that remain active. Must be greater than zero.
retrieval_window.trigger_usage_ratio 0.80 Provider-prepared input ratio that starts automatic archival.
retrieval_window.target_usage_ratio 0.60 Post-archive target ratio. Validation requires 0.01 <= target < trigger <= 1 because the planner uses whole percentages.
retrieval_window.history_tool_required true Must remain true in this release. Bamboo fails before archival unless the effective callable catalog contains session_history_current.
retrieval_window.fallback_strategy none none fails closed when retrieval archival is unavailable. summary explicitly opts into the legacy summarizer fallback.

Retrieval-window commits require runtime persistence and are checkpointed before the archived state is published or a provider request is sent. The checkpoint compares the exact pre-archive base while holding the Session write lock; a concurrent durable transcript change performs no archive write and causes the engine to rebase, re-account, replan, and rebuild the provider request before a bounded retry. Selecting retrieval_window with fallback_strategy: "none" for a Session that already has a conversation summary is rejected immediately; start a new Session or explicitly retain summary fallback. This first runtime slice supports automatic pre-turn archival, explicit model-requested archival through the argument-free archive_context tool, and critical overflow recovery through the same summary-free boundary. compact_context remains a summary-specific control: under retrieval_window, it fails closed unless fallback_strategy: "summary" explicitly opts into the legacy summarizer. Manual requests that are already at or below the target are durably consumed as no-ops so they do not loop after restart. Failed archive checkpoints remain retryable and never publish partially archived state. Candidate fitting projects the exact post-boundary provider request. Provider- native reasoning/tool-search replay and prior model-context ledger bytes that the boundary resets count toward trigger pressure, but are reclaimed once (not misclassified as permanently fixed prompt cost) when calculating the retained target. The raw Session transcript remains authoritative. Memory is selective context, not a substitute for exact history.

Memory / auto-dream / gardener

Key memory (Option<MemoryConfig> — absent means every default below applies). All the dream/gardener toggles default on; the values below are the shipped defaults, so an empty {} is already reasonable:

Field Default What it does
background_model None Model used for memory extraction/consolidation background work; falls back to the primary model.
summary_target_ratio 0.20 Desired durable conversation-summary size relative to the raw source tokens represented by it. Hierarchical reducers retain this global ratio instead of applying it again at every level.
summary_safe_window_percent 80 Maximum share of the summarization model's total context window used by each fully rendered map/reduce request, including requested output and the tokenizer safety margin.
auto_dream_enabled true Distill conversation stretches into candidate memories + notebook entries as the session runs.
auto_dream_interval_secs 1800 How often the dream pass runs.
project_prompt_injection true Inject relevant project-scoped memory into the system prompt.
relevant_recall true Retrieve relevant durable memories for the current turn.
relevant_recall_rerank false Rerank recalled memories (extra model call) before injecting.
project_first_dream true Prefer project-scoped memory on a session's first dream pass.
ledger_agenda_injection / ledger_gardener_enabled / ledger_distillation_enabled true Personal-assistant ledger subsystem toggles.
ledger_gardener_interval_secs 21600 (6h) Ledger gardener cadence.
gardener_enabled true Background job that splits "multi-topic blob" memories; calls no LLM when its deterministic pre-screen finds no candidates.
gardener_interval_secs 86400 (daily) Gardener cadence.
gardener_volume_trigger 25 Run early once this many new memories have accrued, instead of waiting for the interval.
gardener_max_splits_per_run / gardener_min_sections 8 / 5 Cost guardrails on one gardener pass.
dedup_gardener_enabled true Background near-duplicate memory merge pass.
dedup_gardener_min_score 0.6 Jaccard similarity threshold to merge.
dedup_gardener_max_merges_per_run 8 Cap per pass.
memory_active_capacity 0 (unbounded/off) Cap on "active" memory count before older ones archive.
capacity_max_archivals_per_run 50 Cap per capacity-enforcement pass.
granularity_freshness_gardener_enabled true Background staleness/granularity pass.

Automatic conversation compression always maps bounded source chunks and then reduces their summaries, even when the selected source would fit in one model request. Large terminal results are reduced into bounded multipart sections; the persisted summary keeps the single overall summary_target_ratio budget.

project_prompt_injection / relevant_recall / relevant_recall_rerank / project_first_dream can also be flipped via env vars — see Environment variables — which is handy for a one-off container run without touching config.json.

auto_dream_enabled/gardener_enabled intentionally consume model tokens when on; turn them off ({"memory": {"auto_dream_enabled": false}}) for a minimal-cost deployment.

Sub-agents + external CLI executors

Key subagents (SubagentsConfig). Sub-agents always run as independent actor subprocesses (crash isolation + real parallelism) — there is no in-process runtime toggle.

Field Purpose
max_concurrent Cap on simultaneously running sub-agents (default: 200).
worker_bin / worker_args Override the sub-agent worker binary/args (defaults to the current bamboo binary's subagent-worker mode).
fabric_dir Where the actor fabric's mailbox/state files live.
executor Which executor spawns a child: "echo" (test stub) | "bamboo_runtime" (default — a full nested Bamboo agent loop) | "claude_code" | "codex".
claude_code_binary Path to the claude binary; None resolves claude via PATH.
claude_code_model --model passed to claude.
claude_code_permission_mode --permission-mode passed to claude (always sent explicitly, even "default", once this executor is selected).
claude_code_inherit_user_config false/unset adds --strict-mcp-config --setting-sources project, sandboxing the child from your personal claude config.
claude_code_forward_env Extra environment variable names forwarded verbatim into the child (on top of a fixed allowlist: HOME/PATH/SHELL/TERM/LANG/LC_*/TMPDIR/USER/LOGNAME).
codex_binary / codex_model Codex executable and optional --model override.
codex_mode "exec" (default, one process per activation) or "app_server" (long-lived JSON-RPC with parent approval relay). Missing app-server capability fails clearly and never downgrades.
codex_auth_mode "inherit" | "api_key" | "custom" | "bamboo"; unset defaults to the recommended "bamboo" parent-provider mode.
codex_base_url Absolute HTTP(S) URL for custom mode only; credentials, query parameters, and fragments are rejected.
codex_wire_api "responses" (the only protocol accepted by supported Codex CLI versions).
codex_provider_key_ref Existing Bamboo provider credential reference used only by custom mode; the key is injected through an environment variable and is not written to the generated Codex config.
codex_forward_env Extra environment names after env_clear(); api_key mode requires an explicit OPENAI_API_KEY, and other modes reject it. CODEX_* and Bamboo's managed provider-key variable are reserved.
codex_sandbox Optional explicit "read-only" | "workspace-write" | "danger-full-access". Unset derives a safe value from the child profile and live parent bypass posture.
codex_approval_policy Exec mode accepts optional "never" | "on-failure"; app-server mode accepts unset or "on-request". Cross-mode combinations are rejected.
codex_network_access Enables network access inside workspace-write; incompatible with an explicit read-only sandbox.
codex_allow_danger_bypass Second gate for disabling the OS sandbox. The live parent must also be in bypass mode; root workers always downgrade and warn.
remote_placements / schedulable_placements Where a sub-agent may run (local / a named Cluster Fabric node) and whether schedules may target it.
mcp_role_allowlist Restrict which MCP servers a sub-agent role may see.
{
  "subagents": {
    "executor": "claude_code",
    "claude_code_model": "claude-sonnet-4-6",
    "claude_code_permission_mode": "acceptEdits",
    "claude_code_forward_env": ["MY_TOOL_TOKEN"]
  }
}

The same claude_code_* and codex_* field sets are duplicated per-agent under ExternalAgentProfile (Config.extra["externalAgents"], bamboo-engine/src/external_agents/config.rs) when you need different external CLI executor settings for different named agents rather than one global default.

The concrete spawn implementation is src/claude_code_executor.rs (ClaudeCodeExecutor): it runs claude --output-format stream-json --input-format stream-json --permission-prompt-tool stdio --replay-user-messages --verbose [--model ...] [--permission-mode ...] [--resume <id>], in a fully env_clear()'d child process (only the allowlist above is passed through) — the session id maps to claude's own --resume via a small claude-code-session.json state file per sub-agent workspace.

For Codex configuration, billing implications, isolation details, and the per-run Bamboo token contract, see codex-executor.md. Lotus validates this section with POST /bamboo/config/validate; its binary Detect action calls POST /bamboo/config/codex/detect, which returns the resolved path and version only after the same preflight used at worker spawn.

MCP servers

On-disk key mcpServers (legacy mcp alias still read), typed field Config.mcp: McpConfig:

{
  "mcpServers": {
    "version": 1,
    "servers": [
      {
        "id": "filesystem",
        "name": "Local filesystem",
        "enabled": true,
        "transport": {
          "type": "stdio",
          "command": "npx",
          "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allow"]
        },
        "request_timeout_ms": 60000,
        "healthcheck_interval_ms": 30000,
        "allowed_tools": [],
        "denied_tools": []
      }
    ]
  }
}

transport is one of three shapes (tagged by type): stdio (command/args/cwd/env/startup_timeout_ms — spawns a child process), sse (url/headers/connect_timeout_ms), or streamable_http (same shape as sse, MCP's newer single-endpoint transport). reconnect controls auto-reconnect backoff (enabled, initial_backoff_ms, max_backoff_ms, max_attempts, 0 = unlimited). allowed_tools/denied_tools filter which of the server's advertised tools are actually exposed (empty allowed_tools = all allowed). See the bamboo mcp CLI verbs for managing this without hand-editing JSON, or examples/mcp_client.rs for wiring a server programmatically via the SDK.

Notifications

Key notifications (NotificationsConfig):

{
  "notifications": {
    "desktop": { "enabled": true },
    "ntfy": { "enabled": true, "base_url": "https://ntfy.sh", "topic": "my-bamboo-alerts", "credential_ref": "notification.ntfy.token", "configured": true },
    "bark": { "enabled": false, "base_url": "https://api.day.app", "credential_ref": "notification.bark.device_key", "configured": false }
  }
}

desktop.enabled: Option<bool> — None auto-detects (on for a standalone bamboo serve, off when running under a --parent-pid sidecar, since the host app usually owns notifications there). ntfy/bark are push-relay channels; ntfy.token/bark.device_key live only in the isolated encrypted credential store. Ordinary config.json and parseable rotated backups contain only stable credential_ref/configured metadata, never plaintext, ciphertext, or a UI mask. Legacy plaintext/ciphertext is migrated idempotently through the recoverable config/credential manifest.

GET /bamboo/config/notifications returns the current credential revision, health, source, channel metadata, and per-channel configured/source/update status without a secret slot. Notification updates use POST /bamboo/config with only expected_revision and notifications in the request. Omitting a secret keeps it, null or "" clears it, and a non-empty string replaces it; masks and client-supplied credential_ref/configured/ciphertext are rejected. Notification changes cannot be combined with another root domain in one call. "notifications": null is an explicit domain reset: both credentials are cleared and notification metadata returns to defaults in the same transaction. bamboo config set notifications.ntfy.token ... and the Bark equivalent route through the same manifest transaction.

All three channels feed the same AgentEvent::Notification category/priority policy — see crates/infra/bamboo-notification.

connect — the IM bridge

Drives sessions from IM platforms (Telegram, Feishu/Lark). Despite Config having a typed connect field, this is NOT stored in config.json — it lives in its own sibling file, ${data_dir}/connect.json, loaded/merged by Config::merge_connect_config and saved by Config::save_connect_config (both in config.rs). A config.json with no connect.json next to it and no legacy inline connect key starts zero background tasks — fully inert by default.

{
  "platforms": [
    {
      "id": "b3f5...",
      "type": "telegram",
      "token": "123456:ABC-DEF...",
      "allow_from": ["123456789"],
      "admin_from": []
    }
  ]
}

Fields (ConnectPlatformConfig): id (stable UUID, auto-backfilled on save — never assume it's present on a hand-written entry), type ("telegram" | "feishu"; unrecognized values are skipped with a startup warning, not a hard failure), token/token_encrypted (bot token, Telegram), app_id (Feishu, not a secret), app_secret/app_secret_encrypted (Feishu), domain (Feishu only — None/"feishu" → open.feishu.cn, "lark" → open.larksuite.com, or an explicit https:// base for self-hosted deployments), allow_from (empty = deny-all — deliberately stricter default than other allowlists in this codebase, since IM bridges are internet-facing by nature), admin_from (parsed, currently unused).

A legacy inline connect key found inside config.json (from before this was split out) is migrated automatically on next load: adopted into connect.json, then stripped from config.json. A corrupt connect.json is quarantined to connect.json.bak and treated as empty (fail-safe — never silently falls back to a stale inline copy).

plugin_trust

Key plugin_trust (PluginTrustConfig) — the trust policy for bamboo plugin install <url> (see Plugins how-to):

{
  "plugin_trust": {
    "trusted_hosts": ["github.com/bigduu/"],
    "trusted_keys": [
      { "label": "nova official", "algorithm": "ed25519", "public_key": "<hex>" }
    ],
    "enforcement": "strict"
  }
}

trusted_hosts — host+path prefixes a url-source install's URL must match to skip --allow-untrusted-host. trusted_keys — ed25519 public keys (hex) trusted to sign plugin bundles (defaults ship the official nova + magpie keys); a bundle signed by one of these skips --allow-unsigned AND, per the trust model, also satisfies the checksum requirement (a verified signature is strictly stronger than a pasted sha256). enforcement — "strict" (default) or "off" (accepts a bool too: true==strict, false==off); "off" is the config-level equivalent of passing --insecure to every url install, for a private/dev instance that never wants confirmation prompts. Local (local_dir/local_archive) installs are never subject to this policy — it only gates network downloads.

Keyword masking

Key keyword_masking (KeywordMaskingConfig { entries: Vec<KeywordEntry> }, each { pattern, match_type: "exact" | "regex", enabled }). Applied as a value-aware scan over the FINAL serialized outbound provider request body (not field-by-field) — every string value matching a pattern is masked before the request leaves the process, catching secrets that end up embedded in tool output, file contents, etc., not just ones typed directly into chat.

Permissions

Lives under the "permissions" key inside Config.extra (the flatten catch-all — not yet promoted to a typed top-level field). Shape (SerializablePermissionConfig, crates/infra/bamboo-permission/src/config.rs): whitelist: Vec<PermissionRule>, enabled: bool, session_grant_duration_secs (default 1800), mode: Option<PermissionMode>, confirm_threshold: Option<RiskLevel>, ask_rules: Vec<String> — glob-ish patterns like "Bash(rm -rf *)" that force a confirmation prompt even under the legacy bypassPermissions mode. The design invariant: bypass skips ordinary prompts but still asks for the user's own ask_rules and a small hard-coded set of catastrophic commands (sudo, curl | sh, dd, rm -rf /, …). The stronger auto mode emits no approval prompt, including for those forced-ask cases, but still enforces explicit policy and platform denials.

Model limits (model_limits.json)

A separate file, ${data_dir}/model_limits.json — user-supplied context/output token limit overrides. Explicit user matches take precedence over provider runtime metadata. The legacy standalone representation is a raw array:

[
  { "model_pattern": "my-custom-model", "max_context_tokens": 136192, "max_output_tokens": 8192 }
]

model_pattern is either an exact model id or a literal substring of the runtime model id; * and other glob characters have no special meaning. Among substring matches, the longest pattern wins.

max_context_tokens is the provider's total input + output context window, not its input allowance alone. Bamboo derives the per-request input limit as:

max_request_input_tokens =
    max_context_tokens - max_output_tokens - safety_margin

safety_margin is optional. When max_output_tokens is omitted, Bamboo derives it from the context window. Provider metadata that exposes separate max_input_tokens and max_output_tokens is normalized to the same total context-window contract before runtime budgeting.

The modular configuration store persists this section in a revisioned {schema_version, revision, data} envelope. Runtime loading accepts both that envelope and the legacy raw array, and root sessions re-read the sidecar at the start of every agent round; an explicit session/child or engine-level TokenBudget remains an intentional higher-priority override. Manage the section through Bamboo's settings API instead of hand-editing it while the server is running.

With no matching user or provider value, Bamboo falls back to a global default of 1M total context / 32K per-request output allowance. There is deliberately no built-in per-model table, so stale hard-coded model names cannot override live provider metadata.

Schedules (schedules.json)

Also a separate file, ${data_dir}/schedules.json (not part of config.json) — timed/cron tasks managed by bamboo schedules list|show|create|delete|run|runs or the /bamboo/schedules HTTP routes. Each entry (ScheduleSpec) has an id/name/enabled, a trigger (Interval | Once | Daily | Weekly | Monthly | Cron), an optional timezone, start_at/end_at bounds, a misfire_policy (what happens if the process was down when a fire was due: RunOnce (default) | Skip | CatchUpAll | CatchUpWindow), an overlap_policy (Allow | Skip | QueueOne, default QueueOne), and run_config (the prompt/session parameters for the fired run). Not meant for hand-editing — use the CLI/HTTP verbs, which validate the trigger shape.

Environment variables

Every BAMBOO_* variable Bamboo reads, grouped by what it affects. All are optional; file config plus built-in defaults cover a fresh install.

Bootstrapping / core:

Var Effect
BAMBOO_DATA_DIR Data directory (default ${HOME}/.bamboo).
BAMBOO_PORT Server port override.
BAMBOO_BIND Server bind address override.
BAMBOO_PROVIDER Default provider override.
BAMBOO_HEADLESS Enable headless auth mode.
BAMBOO_WORKERS Actix worker-count override (CLI-level).

Provider API keys (in-memory only — never persisted to config.json, even after bamboo config set; the point is a plaintext-key-free config file for Docker/CI/secret-manager deploys):

BAMBOO_OPENAI_API_KEY, BAMBOO_ANTHROPIC_API_KEY, BAMBOO_GEMINI_API_KEY.

Memory toggles (override the matching memory.* config field): BAMBOO_MEMORY_PROJECT_PROMPT_INJECTION, BAMBOO_MEMORY_RELEVANT_RECALL, BAMBOO_MEMORY_RELEVANT_RECALL_RERANK, BAMBOO_MEMORY_PROJECT_FIRST_DREAM.

Server hardening / networking:

Var Effect
BAMBOO_RATE_LIMIT_PER_SECOND / BAMBOO_RATE_LIMIT_BURST Governor rate-limiter tuning.
BAMBOO_RATE_LIMIT_TRUST_XFF / BAMBOO_RATE_LIMIT_TRUSTED_HOPS Trust X-Forwarded-For behind N reverse-proxy hops.
BAMBOO_CSP Full Content-Security-Policy header override.
BAMBOO_CSP_CONNECT_SRC Just the CSP connect-src directive.
BAMBOO_CORS_ALLOW_ORIGINS CORS allowlist.
BAMBOO_ENABLE_DEV_ENDPOINTS Gate dev-only HTTP endpoints.
BAMBOO_WS_AUTH_DEADLINE_MS WS v2 auth handshake timeout.

Workspace / paths:

Var Effect
BAMBOO_WORKSPACE_DIR Project/workspace directory override.
BAMBOO_WORKSPACE_ROOT Root dir for session workspaces (default {data_dir}/workspaces).
BAMBOO_WORKSPACE_CONFINE 1/true/yes forces workspace paths to stay under BAMBOO_WORKSPACE_ROOT; implied when that var is set.
BAMBOO_SKILL_MODE Active skill mode override.

Provider/runtime tuning:

Var Effect
BAMBOO_LLM_MAX_RETRIES / BAMBOO_LLM_RETRY_BASE_DELAY_MS / BAMBOO_LLM_RETRY_MAX_DELAY_MS LLM HTTP request retry policy.
BAMBOO_RESPONSES_DEBUG / BAMBOO_RESPONSES_DEBUG_FILE Dump raw OpenAI Responses API traffic to a file for debugging.
BAMBOO_PYTHON Python interpreter override.

Windows-specific: BAMBOO_WINDOWS_BASH_PATH, BAMBOO_WINDOWS_CMD_TRACE (also honors BODHI_WINDOWS_CMD_TRACE).

Secrets / plugins / broker:

Var Effect
BAMBOO_CONFIG_ENCRYPTION_KEY Master AES-256 key for at-rest secret encryption — see Encryption at rest.
BAMBOO_BROKER_TOKEN Auth token for bamboo broker/broker-agent subcommands.
BAMBOO_PLUGIN_SERVICE_CONFIG Config path passed into a plugin service's own subprocess.
BAMBOO_FRONTEND_PACKAGE Override the bundled frontend static package path.

Everything above is read via plain std::env::var, so it can also be set through your process manager / Docker Compose / systemd unit rather than exported in a shell.

Secrets and masking

Every secret field (providers.*.api_key, provider_instances.*.api_key, notifications.ntfy.token, notifications.bark.device_key, connect.platforms[].token/.app_secret, subagents.broker.token, cluster_fabric node SSH credentials, secret env_vars entries) follows one contract everywhere it's read or written:

  • Read (GET/bamboo config): a configured secret is never echoed back plaintext. It's replaced with exactly the literal string ****...****; if the field isn't configured at all, the key is omitted entirely (not sent as "").

  • Write (PATCH/bamboo config set): a submitted value counts as "keep the existing secret unchanged" if and only if, after trimming, it consists entirely of * and/or . characters — i.e. it matches the masked placeholder shape exactly. This is a whole-value check, not a substring check: is_masked_api_key() in crates/infra/bamboo-config/src/patch.rs. An empty string explicitly clears the secret. Anything else — including a string that still starts with the placeholder because a UI's prefill wasn't fully cleared before pasting (e.g. ****...****sk-newkey123) — is treated as a real new secret and applied.

    This whole-value rule is deliberate: an earlier substring-based check (fixed as issue #430) could silently discard a user's pasted key when the placeholder wasn't fully selected/overwritten first. Any client embedding Bamboo's settings UI must never pre-fill an editable secret field with the masked placeholder — leave it blank to mean "keep."

Encryption at rest

Every *_encrypted field uses AES-256-GCM (crates/infra/bamboo-config/src/encryption.rs); on disk the ciphertext is stored as hex(nonce):hex(ciphertext), a fresh random nonce per encryption. The master key is resolved once per process, in priority order:

  1. BAMBOO_CONFIG_ENCRYPTION_KEY — hex-encoded, must decode to exactly 32 bytes. Highest priority; use this for reproducible/ephemeral deployments (containers, CI) where you manage the key externally.
  2. Key file ${data_dir}/.bamboo_encryption_key — hex-encoded 32 bytes, written with 0600 permissions atomically on Unix.
  3. Machine-derived key — SHA-256 of a machine identifier (/etc/machine-id on Linux, registry MachineGuid on Windows, ioreg IOPlatformUUID on macOS) with domain separation, then persisted to the key file so subsequent runs don't re-derive it.
  4. Last resort — cryptographically random 32 bytes, persisted to the key file.

Backup/disaster-recovery implication: losing the key file on a host with no stable machine identifier (and no BAMBOO_CONFIG_ENCRYPTION_KEY set) makes every *_encrypted field in that data directory permanently undecryptable — back up .bamboo_encryption_key alongside config.json if you back up your Bamboo data directory at all.

Corrupt-config recovery

If ${data_dir}/config.json exists but fails to parse, Config::from_data_dir does not crash or silently reset to defaults — it runs a recovery flow (issue #493 and predecessors), roughly:

  1. Quarantine the unparseable original by copying it to config.json.corrupted.<timestamp> (the corrupt file is never deleted or moved — the original stays at config.json untouched).
  2. Recover, trying strategies in order:
    • Salvage — parse the corrupt file as generic JSON and adopt each top-level key individually onto the best available baseline, keeping a key only if the whole Config still deserializes with it applied.
    • Backup — if salvage found nothing usable, fall back to config.json.bak, .bak.1, .bak.2 (newest first; 3 generations are kept, rotated on every successful save).
    • Defaults — if neither works, a fresh default Config.
  3. The recovered config is tagged in memory (recovery_status, never persisted) with which strategy produced it and which fields were salvaged.
  4. The recovered config is never auto-saved. Config::save_to_dir refuses to write while recovery is unconfirmed, so the quarantined corrupt original on disk is preserved until something explicitly calls Config::confirm_recovery() / confirm_recovery_and_save_to_dir() (the settings UI/CLI does this after showing the user what was recovered).

Net effect: a corrupted config.json never causes silent data loss — you always get either your own values back (salvage/backup) or an explicit, confirmable prompt before anything is overwritten.