# Flynn Configuration
# Copy to ~/.config/flynn/config.yaml and customize

# Log verbosity: debug | info | warn | error | silent (default: info)
# Set to 'debug' to see model fallback details.
# log_level: info

telegram:
  bot_token: ${FLYNN_TELEGRAM_TOKEN}
  allowed_chat_ids: []  # Add your Telegram chat ID
  # require_mention: false  # Default false: respond to all allowed group messages

# Optional: Signal via signal-cli
# signal:
#   account: "+15551234567"
#   signal_cli_path: signal-cli
#   allowed_numbers: []      # Empty = allow all DMs
#   allowed_group_ids: []    # Empty = no groups
#   require_mention: true
#   mention_name: flynn
#   poll_interval_ms: 5000
#   send_timeout_ms: 15000

# Optional: Mattermost
# mattermost:
#   server_url: ${MATTERMOST_SERVER_URL}
#   bot_token: ${MATTERMOST_BOT_TOKEN}
#   allowed_channel_ids: [] # Empty = allow all channels (pairing/mention rules still apply)
#   require_mention: true
#   mention_name: flynn
#   poll_interval_ms: 3000

# Optional: Microsoft Teams (Bot Framework)
# teams:
#   app_id: ${TEAMS_APP_ID}
#   app_password: ${TEAMS_APP_PASSWORD}
#   allowed_conversation_ids: []   # Empty = allow all conversations
#   require_mention: true

# Optional: Google Chat
# google_chat:
#   service_account_key_file: ~/.config/flynn/google-chat-service-account.json
#   # or inline via env var expansion:
#   # service_account_json: ${GOOGLE_CHAT_SERVICE_ACCOUNT_JSON}
#   webhook_token: ${GOOGLE_CHAT_WEBHOOK_TOKEN}
#   allowed_space_names: []         # Empty = allow all spaces
#   require_mention: true

# Optional: iMessage via BlueBubbles
# bluebubbles:
#   endpoint: http://localhost:1234
#   api_key: ${BLUEBUBBLES_API_KEY}
#   webhook_token: ${BLUEBUBBLES_WEBHOOK_TOKEN}
#   allowed_chat_guids: []          # Empty = allow all chats
#   require_mention: true
#   mention_name: flynn

# Optional: LINE
# line:
#   channel_access_token: ${LINE_CHANNEL_ACCESS_TOKEN}
#   channel_secret: ${LINE_CHANNEL_SECRET}
#   allowed_source_ids: []           # Empty = allow all users/groups/rooms
#   require_mention: true
#   mention_name: flynn

# Optional: Feishu / Lark
# feishu:
#   app_id: ${FEISHU_APP_ID}
#   app_secret: ${FEISHU_APP_SECRET}
#   webhook_token: ${FEISHU_WEBHOOK_TOKEN}
#   allowed_chat_ids: []             # Empty = allow all chats
#   require_mention: true
#   mention_name: flynn
#   endpoint: https://open.feishu.cn

# Optional: Zalo
# zalo:
#   oa_access_token: ${ZALO_OA_ACCESS_TOKEN}
#   webhook_token: ${ZALO_WEBHOOK_TOKEN}
#   allowed_user_ids: []             # Empty = allow all users
#   require_mention: true
#   mention_name: flynn
#   endpoint: https://openapi.zalo.me

server:
  # Tailscale Serve config (optional). Enable `serve: true` to expose the
  # gateway to your tailnet via `tailscale serve`.
  tailscale:
    serve: false
  localhost: false
  port: 18800
  # Maximum inbound HTTP request body size (bytes) for webhooks/Gmail push.
  max_request_body_bytes: 1048576
  ws_rate_limit:
    enabled: true
    capacity: 30
    refill_per_sec: 15
    max_violations: 8
    violation_window_ms: 10000
  # Per-session FIFO lane queue for gateway requests.
  queue:
    mode: collect      # collect | followup | steer | steer_backlog | interrupt
    cap: 50            # max queued (pending) requests per session lane
    overflow: drop_old # drop_old | drop_new
    debounce_ms: 0     # delay before starting next queued request
    summarize_overflow: true
    overrides:
      channels: {}     # e.g. ws: { mode: followup, cap: 10, debounce_ms: 100 }
      sessions: {}     # e.g. ws:vip-user: { mode: interrupt, overflow: drop_new }
  # Companion-node capability negotiation surface (default disabled).
  nodes:
    enabled: false
    allowed_roles: [companion]
    feature_gates: {}
    location:
      enabled: false
    push:
      enabled: false
  # Optional WebChat push subscription endpoints for PWA notifications.
  # Set `enabled: true` and provide a VAPID public key for PushManager.
  webchat_push:
    enabled: false
    # vapid_public_key: ${WEBCHAT_VAPID_PUBLIC_KEY}
    max_subscriptions: 5000
  # Local-network service discovery (mDNS/Bonjour). Keep disabled by default.
  # Requires server.localhost: false so LAN clients can actually connect.
  discovery:
    enabled: false
    service_name: flynn-gateway
    service_type: _flynn._tcp
    txt: {}

models:
  # ── Model tiers ────────────────────────────────────────────────────
  # Each tier (default, fast, complex, local) defines a primary model.
  # When an Anthropic tier fails, Flynn automatically tries the same
  # model via GitHub Models before falling through to the global chain.
  #
  # You can override auto-fallback with an inline `fallback:` block:
  #
  #   default:
  #     provider: anthropic
  #     model: claude-sonnet-4-20250514
  #     fallback:                          # ← inline per-tier fallback
  #       provider: openai
  #       model: gpt-4o
  #
  # ── Fallback order ─────────────────────────────────────────────────
  # 1. Primary client for the tier
  # 2. Auto same-model fallback via GitHub Models (Anthropic tiers only,
  #    skipped when an inline `fallback:` block is present)
  # 3. Inline `fallback:` client (if configured)
  # 4. Global fallback_chain (tried in order)
  #
  default:
    provider: openai
    model: gpt-5.2
    auth_mode: api_key                 # auto | api_key | oauth (provider-specific)
    api_key: ${OPENAI_API_KEY}
    # use_oauth: false                 # compat alias for auth_mode: oauth
    # api_keys: ["${ANTHROPIC_API_KEY_PRIMARY}", "${ANTHROPIC_API_KEY_SECONDARY}"] # Optional rotation pool
    # supports_audio: false            # Override native audio detection per tier
  fast:
    provider: anthropic
    model: claude-haiku-4-5-20251001
  complex:
    provider: anthropic
    model: claude-opus-4-6-20250715
  local:
    provider: ollama
    model: glm-4.7-flash

  # ── Global fallback chain ──────────────────────────────────────────
  # Entries can be tier names (default, fast, complex, local) or keys
  # from local_providers below. Tried in order after per-tier fallbacks
  # are exhausted.
  fallback_chain: [local]

  # ── Named providers (optional) ─────────────────────────────────────
  # Define additional providers that can be referenced by name in
  # fallback_chain. Useful for secondary API accounts or self-hosted
  # endpoints that aren't tied to a specific tier.
  #
  # Use /backend <name> in the TUI to switch between these providers
  local_providers:
    ollama:
      provider: ollama
      model: glm-4.7-flash
      endpoint: http://localhost:11434
    llamacpp:
      provider: llamacpp
      model: gpt-oss-20b
      endpoint: http://localhost:8080
  #
  # Then reference them in fallback_chain:
  #   fallback_chain: [ollama, llamacpp, local]

# Optional: external CLI backends for non-tool conversation turns.
# - native remains available unless you disable it.
# - set `default` to route normal text turns to a backend by default.
# - per-agent override is available via `agent_configs.<name>.backend`.
# backends:
#   default: codex                  # claude_code | opencode | codex | gemini | pi_embedded
#   native:
#     enabled: true
#   claude_code:
#     enabled: false
#     # path: /usr/local/bin/claude
#     # args: ["--print", "{prompt}"]
#     # timeout_ms: 120000
#   opencode:
#     enabled: false
#     # path: /usr/local/bin/opencode
#     # args: ["run", "--format", "default", "{prompt}"]
#     # timeout_ms: 120000
#   codex:
#     enabled: false
#     # path: /usr/local/bin/codex
#     # args: ["-p", "{prompt}"]
#     # timeout_ms: 120000
#   gemini:
#     enabled: false
#     # path: /usr/local/bin/gemini
#     # args: ["-p", "{prompt}"]
#     # timeout_ms: 120000
#   pi_embedded:
#     enabled: false
#     # timeout_ms: 120000
#     # no_tools_mode: true         # keep Pi path text-only in canary; force native for tool-like prompts
#     # model: openclaw-default     # optional model/session selector passed to Pi runtime
#     # system_prompt_mode: hybrid  # flynn | pi_default | hybrid
#     # module: "@mariozechner/pi-agent-core"   # optional module override

# Optional: Kubernetes / homelab awareness tools (k8s.pods, k8s.deployments, k8s.logs)
# k8s:
#   enabled: false
#   kubectl_path: kubectl
#   default_namespace: default
#   allowed_namespaces: []  # Empty = allow any namespace; set to restrict access.

# Optional: Browser automation tools (browser.navigate/screenshot/click/type/content/eval/evaluate)
# Requires a local Chrome/Chromium install or a remote CDP endpoint.
# browser:
#   enabled: true
#   # executable_path: /usr/bin/google-chrome
#   # ws_endpoint: ws://127.0.0.1:9222/devtools/browser/<id>
#   headless: true
#   max_pages: 5
#   default_timeout: 30000
#
# Tool policy reminder:
# - `tools.profile: coding` or `tools.profile: full` must allow browser.* tools.
# - `tools.deny` patterns can still block browser.* even when browser.enabled is true.

# ── Web Search ───────────────────────────────────────────────────────
# Configure web.search tools.
# - provider: brave   -> enables web.search + web.search.news (requires api_key)
# - provider: searxng -> enables web.search (requires endpoint)
#
# web_search:
#   provider: brave
#   api_key: "${BRAVE_API_KEY}"
#   max_results: 5
#   # endpoint: "http://searxng:8080"     # Used when provider=searxng (or http://localhost:18803 if Flynn runs on host)
#   # fallback_endpoint: "https://searxng.homelab.local"  # Optional backup endpoint for provider=searxng
#
# Optional local Brave Search MCP container (for MCP workflows only):
#   docker compose --profile search up -d brave-search
# Optional local SearXNG container:
#   docker compose --profile search up -d searxng
# Built-in web.search* tools call Brave/SearXNG HTTP endpoints directly.

hooks:
  confirm:
    - shell.*
    - process.start
    - process.kill
    - browser.*
    - message.send
    - cron.create
    - cron.delete
    - file.write
    - file.patch
  log:
    - web.*
    - file.read
  silent:
    - notify

# ── Safety Notes ─────────────────────────────────────────────────────
# - Tool policy (tools.profile/allow/deny) controls which tools are available.
# - Skills can declare capability permissions in skills/<name>/manifest.json under `permissions`.
#   Those permissions are enforced at runtime when requests are routed into a skill context.
# - See: docs/security/SAFE_PERSONAL_AGENT.md

agents:
  # In full-access mode, sensitive operations are gated by HookEngine confirmation
  # (instead of requiring temporary /elevate windows).
  sensitive_mode: confirm_without_elevation

# ── Memory / Embeddings ──────────────────────────────────────────────
# Enable hybrid keyword + vector search using local Ollama embeddings.
memory:
  enabled: true
  auto_extract: true
  embedding:
    enabled: true
    provider: ollama
    model: nomic-embed-text
    endpoint: http://localhost:11434
    chunk_size: 512
    chunk_overlap: 50
    top_k: 5
    hybrid_weight: 0.7

# ── Prompt Assembly ───────────────────────────────────────────────────
# Tune how much context Flynn loads into the system prompt.
#
# prompt:
#   search_dirs: []
#   extra_sections: []
#   context_level: normal  # minimal | normal | detailed | debug

# Optional: named agent profiles and routing.
# Each agent can pin model tier, tool profile, sandbox mode, and execution backend.
#
# agent_configs:
#   assistant:
#     system_prompt: You are helpful.
#     model_tier: default
#     backend: native          # native | codex | claude_code | opencode | gemini | pi_embedded
#   coder:
#     system_prompt: Write code.
#     model_tier: complex
#     backend: codex
#     sandbox: true
#
# routing:
#   default_agent: assistant
#   channels:
#     telegram: assistant
#   senders:
#     telegram:admin: coder

# ── Context Compaction ────────────────────────────────────────────────
# Optional proactive context monitoring and checkpointing.
#
# compaction:
#   enabled: true
#   threshold_pct: 80
#   keep_turns: 4
#   summary_max_tokens: 1024
#   importance_threshold: 1
#   proactive:
#     enabled: false
#     warn_pct: 75
#     checkpoint_pct: 85
#     auto_compact_pct: 95
#     checkpoint_cooldown_ms: 300000
#     memory_namespace: session/checkpoints

# skills:
#   # Registry catalog source for discovery and install-by-id:
#   # local path or HTTPS URL. Can also be set via FLYNN_SKILLS_REGISTRY_SOURCE.
#   registry_source: ~/.config/flynn/skills-registry.json
#   # Global installer execution policy.
#   # disabled: never run installer commands (default)
#   # enabled: allow command execution only with --execute --confirm
#   installation_execution: disabled
#   # Allow shell-based installer runner when --runner shell is requested.
#   allow_shell_runner: false
#   # Allowlist command patterns for shell runner (`*` wildcard supported).
#   # Empty list means no shell commands are allowed.
#   shell_runner_allowlist: []
#   # Governance metadata for shell-runner allowlist and rollout decisions.
#   shell_runner_governance:
#     owner: "skills-team"               # Required when allow_shell_runner is true
#     review_cadence_days: 7              # Review `skills rollout-status` at this cadence
#     promotion_min_success_rate: 0.9     # Rollout threshold for broader enablement

# ── Automation ──────────────────────────────────────────────────────
# Uncomment and configure any automation sources you need.

# automation:
#   # shared_session: keep one session per cron job/webhook name.
#   # isolated_job: create a fresh session per cron trigger/webhook request.
#   # announce: create a fresh announce-style run per trigger (no shared automation history).
#   delivery_mode: shared_session
#   reactions:
#     - name: boss-email
#       on: [gmail]
#       filter:
#         contains: "boss@company.com"
#       run: |
#         Summarize this email and propose next actions.
#
#         {{text}}
#
#     - name: github-main-push
#       on: [webhook]
#       filter:
#         metadata:
#           webhookName: github-push
#           body.ref: refs/heads/main
#       run: |
#         Summarize this deploy-relevant push:
#         {{metadata.body.head_commit.message}}
#   cron:
#     - name: daily-summary
#       schedule: "0 9 * * *"
#       message: "Give me a summary of today's tasks"
#       output:
#         channel: telegram
#         peer: "123456789"
#       once_per_local_day: false
#
#   # Optional built-in morning briefing job (auto-registered as a cron job)
#   daily_briefing:
#     enabled: false
#     name: daily-briefing
#     schedule: "0 8 * * *"
#     timezone: America/New_York
#     dedupe_per_local_day: true
#     output:
#       channel: telegram
#       peer: "123456789"
#     model_tier: fast
#     prompt: |
#       Create my daily briefing.
#       Summarize today's calendar, unread/important email, and top pending tasks.
#
#   # Optional scheduled MinIO -> memory synchronization
#   minio_sync:
#     enabled: false
#     interval: "6h"
#     run_on_start: false
#     notify_on_success: false
#     notify:
#       channel: telegram
#       peer: "123456789"
#     tasks:
#       - prefix: "knowledge/"
#         namespace_base: "global/knowledge/minio"
#         mode: append
#         max_objects: 20
#         max_chars_per_object: 8000
#         force: false
#
#   webhooks:
#     - name: github-push
#       secret: "whsec_..."
#       message: "GitHub push to {{json.repository.full_name}}: {{json.head_commit.message}}"
#       output:
#         channel: telegram
#         peer: "123456789"
#
#   gmail:
#     enabled: false
#     credentials_file: ~/.config/flynn/gmail-credentials.json
#     token_file: ~/.config/flynn/gmail-token.json
#     # Authenticate with: flynn gmail-auth
#     # (requests gmail.settings.basic + gmail.readonly for filter create + inbox tools)
#
#     # Optional Pub/Sub delivery
#     # Push mode: configure a topic and a push subscription that POSTs to /gmail/push
#     pubsub_topic: projects/your-project/topics/gmail-push
#     disable_push: false
#
#     # Pull mode: no inbound webhook required (requires Application Default Credentials)
#     pubsub_subscription_id: projects/your-project/subscriptions/gmail-pull
#     pubsub_pull_interval: "60s"
#     pubsub_max_messages: 10
#     watch_labels: [INBOX]
#     poll_interval: "60s"
#     message: "New email from {{from}}: {{subject}}\n\n{{snippet}}"
#     output:
#       channel: telegram
#       peer: "123456789"
#
#   heartbeat:
#     enabled: false
#     interval: "5m"
#     notify_cooldown: "30m"
#     checks: [gateway, model, channels, memory, disk, process_memory, backup, provider_errors]
#     notify:
#       channel: telegram
#       peer: "123456789"
#     failure_threshold: 2
#     disk_threshold_mb: 100
#     process_memory_threshold_mb: 1500
#     backup_failure_threshold: 1
#     provider_error_rate_threshold: 0.5
#     provider_error_min_calls: 5

automation:
  heartbeat:
    enabled: true
    interval: "5m"
    notify_cooldown: "30m"
    checks: [gateway, model, channels, memory, disk, process_memory, backup, provider_errors]
    failure_threshold: 2
    disk_threshold_mb: 100
    process_memory_threshold_mb: 1500
    backup_failure_threshold: 1
    provider_error_rate_threshold: 0.5
    provider_error_min_calls: 5

# ── Backup ──────────────────────────────────────────────────────────
# Snapshot sessions.db, vectors.db (optional), and memory/ into a tarball.
# If MinIO is enabled, upload with `mc` using ephemeral credentials.
# LINE/Zalo adapters also reuse backup.minio credentials for binary-attachment
# URL sharing when those channels are configured.
#
# backup:
#   enabled: false
#   # Optional cron schedule (takes precedence over interval), e.g. nightly at 2 AM.
#   schedule: "0 2 * * *"
#   interval: "24h"
#   run_on_start: false
#   notify:
#     channel: telegram
#     peer: "123456789"
#   failure_threshold: 1
#   notify_recovery: true
#   local_dir: ~/.local/share/flynn/backups
#   include_vectors: true
#   minio:
#     enabled: false
#     endpoint: localhost:9000
#     access_key: ${MINIO_ACCESS_KEY}
#     secret_key: ${MINIO_SECRET_KEY}
#     bucket: flynn-backups
#     prefix: flynn
#     secure: true

# ── Session Lifecycle ───────────────────────────────────────────────
# sessions:
#   ttl: "30d"
#   end_summary:
#     enabled: false
#     tier: fast
#     max_messages: 50
#     max_input_chars: 20000
#     max_tokens: 512
#     write_to_memory: true
#     memory_namespace: session/summaries

# ── Audio ────────────────────────────────────────────────────────────
# Configure a Whisper-compatible endpoint for audio transcription.
# Models that support native audio input (Gemini, OpenAI, GitHub) will
# receive raw audio directly; others fall back to this endpoint.
#
# For local transcription with whisper.cpp:
#   docker run -d --name whisper-server -p 18801:8080 \
#     ghcr.io/ggml-org/whisper.cpp:main \
#     --model /app/models/ggml-base.en.bin \
#     --host 0.0.0.0 --port 8080 --convert --language en \
#     --inference-path /v1/audio/transcriptions
#
# audio:
#   enabled: true
#   provider:
#     type: custom                            # openai, groq, ollama, llamacpp, custom
#     endpoint: "http://localhost:18801/v1/audio/transcriptions"
#     api_key: "${WHISPER_API_KEY}"            # Optional Bearer token
#     model: "whisper-1"                       # Model name (default: whisper-1)
#   talk_mode:
#     enabled: false
#     wake_phrase: "hey flynn"
#     timeout_ms: 120000
#     allow_manual_toggle: true

# ── Text-to-Speech (TTS) Output ──────────────────────────────────────
# Optional voice output for assistant replies. Uses an OpenAI-compatible
# /v1/audio/speech endpoint and attaches audio to channel replies.
#
# tts:
#   enabled: false
#   enabled_channels: [telegram, whatsapp, discord] # Empty = all channels
#   provider:
#     type: openai                                  # openai | custom
#     endpoint: "https://api.openai.com/v1/audio/speech"
#     api_key: "${OPENAI_API_KEY}"                  # Optional Bearer token
#     model: "gpt-4o-mini-tts"
#     voice: "alloy"
#     format: "mp3"                                 # mp3 | wav | opus

# ── Sub-Agent Configs ────────────────────────────────────────────────
# Named agent configurations for delegation via agent.delegate tool.
# Each agent gets a focused system prompt, model tier, and tool profile.
#
# agent_configs:
#   research:
#     model_tier: default
#     tool_profile: messaging
#     system_prompt: |
#       You are a research agent. Your job is to find, verify, and synthesize
#       information from the web. Be thorough but concise. Cite sources when
#       possible. Return structured findings — not conversational filler.
#       Use web.search to find sources and web.fetch to read them.
#
#   code:
#     model_tier: complex
#     tool_profile: coding
#     system_prompt: |
#       You are a code agent. Your job is to read, write, debug, refactor,
#       and review code. You have full access to the filesystem and shell.
#       Be precise. Read files before modifying them. Run tests after changes.
#       Use file.edit/file.patch for surgical edits, not full file rewrites.
#       Commit conventions: conventional commits, small atomic changes.
#
#   comms:
#     model_tier: fast
#     tool_profile: messaging
#     system_prompt: |
#       You are a communications agent. Your job is to draft messages,
#       summarize emails, triage inbox items, and prepare quick replies.
#       Be concise and match the operator's tone. Skip marketing emails.
#       Never send messages without explicit instruction — draft only.

# ── Councils Pipeline ────────────────────────────────────────────────
# Deterministic dual-council orchestration (D/P groups) with bridge-only
# exchange and a deterministic meta merge. Requires matching agent_configs
# for the configured role names below.
#
# councils:
#   enabled: false
#   defaults:
#     max_rounds: 2
#     ideas_per_round: 6
#     top_ideas_for_bridge: 3
#     bridge_packet_max_chars: 2500
#     bridge_field_max_bullets: 6
#     bridge_entry_max_chars: 300
#     novelty_delta_threshold: 10
#     repetition_threshold: 70
#   strict_grounding: false
#   strict_meta_validation: true
#   groups:
#     D:
#       arbiter_agent: council_d_arbiter
#       freethinker_agent: council_d_freethinker
#       model_tier: complex
#       group_prompt_prefix: Optimize for feasibility and speed-to-test. Prefer boring-but-true.
#       novelty_bias: low
#       risk_tolerance: low
#       forbidden_approaches:
#         - moonshots
#         - handwavy AI claims
#         - unverified assumptions
#     P:
#       arbiter_agent: council_p_arbiter
#       freethinker_agent: council_p_freethinker
#       model_tier: complex
#       group_prompt_prefix: Optimize for reframing and non-obvious leverage. Weird is fine; label speculation.
#       novelty_bias: high
#       risk_tolerance: high
#       forbidden_approaches:
#         - incremental tweaks
#         - obvious best practices
#         - purely conventional solutions
#   meta_arbiter_agent: council_meta_arbiter
#   meta_model_tier: complex
#   # scaffold_path: docs/councils/ai-council-production-scaffold.json

# Optional: explicit intent rules for agent routing.
# If enabled, these rules are evaluated before default sender/channel routing.
# The research agent can already be auto-routed by prefix (`research ...`, `look up ...`)
# when agent_configs.research exists and intents are disabled.
#
# intents:
#   enabled: true
#   match_threshold: 0.7
#   rules:
#     - name: route-research
#       patterns: ["research *", "look up *", "lookup *"]
#       target:
#         type: agent
#         name: research
#       priority: 10
#       enabled: true
