{"repo": "1024xengineer/bytemind", "files": {"www/api-key.md": {"f259496ed7f959b3400c57e2d3d4d0a548d4b6a3": {"t": 1785674069.634893, "n": 5745, "h": "7eb40fa4fb2997ba", "c": "# Get API Key\n\nMany model providers offer an \"OpenAI-compatible API\". To configure ByteMind, collect these four values from your provider:\n\n| Info | ByteMind field | Example |\n| ---- | -------------- | ------- |\n| Provider type | `provider.type` | `openai-compatible` |\n| API endpoint | `provider.base_url` | `https://api.deepseek.com` |\n| Model ID | `provider.model` | `deepseek-v4-flash` |\n| API key | `provider.api_key` | `sk-...` |\n\nThis page uses DeepSeek as an example. Other OpenAI-compatible providers follow the same general flow: find the API key first, then find the base URL and model ID.\n\n## Step 1: Open the Provider Console\n\nOpen [DeepSeek Platform](https://platform.deepseek.com/api_keys), sign in, and go to the API Keys page.\n\n![DeepSeek API Keys page](./zh/assets/api_key1.png)\n\nIf you do not have usable balance, check your balance or top-up status first. API calls are usually billed by token usage. DeepSeek's official documentation says fees are deducted from topped-up or granted balance; prices may change, so use the [Models & Pricing](https://api-docs.deepseek.com/quick_start/pricing) page as the source of truth.\n\n## Step 2: Create an API Key\n\nCreate a new key on the API Keys page. After creation, copy the full key and store it somewhere safe on your machine. You will use it when configuring ByteMind.\n\n![DeepSeek API Keys page](./zh/assets/api_key2.png)\n\n![DeepSeek API Keys page](./zh/assets/api_key3.png)\n\nDo not share your API key with others, and do not commit it to a public repository. For beginners, writing it into your local `~/.bytemind/config.json` is the most direct option; once you are comfortable, you can switch to environment variables.\n\n## Step 3: Read the API Docs\n\nOpen DeepSeek's [First API Call](https://api-docs.deepseek.com/) documentation and focus on these fields:\n\n![DeepSeek API documentation](./zh/assets/deepseek_api.png)\n\n| Doc name | ByteMind config | DeepSeek example |\n| -------- | --------------- | ---------------- |\n| `base_url (OpenAI)` | `provider.base_url` | `https://api.deepseek.com` |\n| | `provider.type` | `openai-compatible` |\n| `api_key` | `provider.api_key` | The key you just created |\n| `model` | `provider.model` | `deepseek-v4-flash` |\n\nAs of 2026-05-07, DeepSeek's official docs recommend OpenAI-format models including `deepseek-v4-flash` and `deepseek-v4-pro`. The older model names `deepseek-chat` and `deepseek-reasoner` are marked for deprecation on 2026-07-24, so new configs should prefer `deepseek-v4-flash` (the web app's \"Fast Mode\") or `deepseek-v4-pro` (the web app's \"Expert Mode\").\n\n## Step 4: Write the ByteMind Config\n\nReplace `YOUR_DEEPSEEK_API_KEY` with the key you just copied.\n\n\n\n\n```powershell\nNew-Item -ItemType Directory -Force \"$env:USERPROFILE\\.bytemind\" | Out-Null\n$config = @'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\n'@\n\n$utf8NoBom = New-Object System.Text.UTF8Encoding($false)\n[System.IO.File]::WriteAllText(\"$env:USERPROFILE\\.bytemind\\config.json\", $config, $utf8NoBom)\n```\n\nThis writes `config.json` as UTF-8 without BOM, so it works consistently in both Windows PowerShell 5.1 and PowerShell 7+.\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n## Step 5: Verify It Works\n\nEnter a specific project directory and start ByteMind:\n\n```bash\nbytemind\n```\n\nTry a very small task:\n\n```text\nDescribe this project in one sentence.\n```\n\nIf the model replies normally, your API key, base URL, and model ID are configured correctly.\n\n## FAQ\n\n**What should `provider.type` be?**\n\nDeepSeek uses the OpenAI-compatible format, so use `openai-compatible`.\n\n**Should `base_url` include `/v1`?**\n\nDeepSeek's official OpenAI-format base URL is `https://api.deepseek.com`. ByteMind appends the default `/chat/completions` path, so do not add `/chat/completions` yourself.\n\n**Can I use `api_key_env` instead of `api_key`?**\n\nYes, and it's safer. Replace `\"api_key\"` with `\"api_key_env\": \"DEEPSEEK_API_KEY\"`, then set the environment variable:\n\n\n\n\n```powershell\n# Temporary (current window only):\n$env:DEEPSEEK_API_KEY = \"sk-...\"\n\n# Permanent (survives reboots):\n[Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\", \"sk-...\", \"User\")\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n:::warning Don't set both `api_key` and `api_key_env`\nIf both are present, `api_key` takes priority and `api_key_env` is ignored. Use one or the other.\n:::\n\n**Can I use any model ID?**\n\nNo. The model ID must exactly match the name in the provider's documentation. For DeepSeek, start with `deepseek-v4-flash`; if you need stronger capability, switch to `deepseek-v4-pro` according to the official docs.\n\n**What if it still fails?**\n\nCheck three things first: whether the key was copied completely, whether your provider balance is usable, and whether `base_url` or `model` has any extra or missing characters. See [Troubleshooting](/troubleshooting) for more checks.\n"}, "0e4d543804070e03cb4f5675e90d2b2f6faf7f84": {"t": 1785749487.8086445, "n": 5745, "h": "7eb40fa4fb2997ba", "c": "# Get API Key\n\nMany model providers offer an \"OpenAI-compatible API\". To configure ByteMind, collect these four values from your provider:\n\n| Info | ByteMind field | Example |\n| ---- | -------------- | ------- |\n| Provider type | `provider.type` | `openai-compatible` |\n| API endpoint | `provider.base_url` | `https://api.deepseek.com` |\n| Model ID | `provider.model` | `deepseek-v4-flash` |\n| API key | `provider.api_key` | `sk-...` |\n\nThis page uses DeepSeek as an example. Other OpenAI-compatible providers follow the same general flow: find the API key first, then find the base URL and model ID.\n\n## Step 1: Open the Provider Console\n\nOpen [DeepSeek Platform](https://platform.deepseek.com/api_keys), sign in, and go to the API Keys page.\n\n![DeepSeek API Keys page](./zh/assets/api_key1.png)\n\nIf you do not have usable balance, check your balance or top-up status first. API calls are usually billed by token usage. DeepSeek's official documentation says fees are deducted from topped-up or granted balance; prices may change, so use the [Models & Pricing](https://api-docs.deepseek.com/quick_start/pricing) page as the source of truth.\n\n## Step 2: Create an API Key\n\nCreate a new key on the API Keys page. After creation, copy the full key and store it somewhere safe on your machine. You will use it when configuring ByteMind.\n\n![DeepSeek API Keys page](./zh/assets/api_key2.png)\n\n![DeepSeek API Keys page](./zh/assets/api_key3.png)\n\nDo not share your API key with others, and do not commit it to a public repository. For beginners, writing it into your local `~/.bytemind/config.json` is the most direct option; once you are comfortable, you can switch to environment variables.\n\n## Step 3: Read the API Docs\n\nOpen DeepSeek's [First API Call](https://api-docs.deepseek.com/) documentation and focus on these fields:\n\n![DeepSeek API documentation](./zh/assets/deepseek_api.png)\n\n| Doc name | ByteMind config | DeepSeek example |\n| -------- | --------------- | ---------------- |\n| `base_url (OpenAI)` | `provider.base_url` | `https://api.deepseek.com` |\n| | `provider.type` | `openai-compatible` |\n| `api_key` | `provider.api_key` | The key you just created |\n| `model` | `provider.model` | `deepseek-v4-flash` |\n\nAs of 2026-05-07, DeepSeek's official docs recommend OpenAI-format models including `deepseek-v4-flash` and `deepseek-v4-pro`. The older model names `deepseek-chat` and `deepseek-reasoner` are marked for deprecation on 2026-07-24, so new configs should prefer `deepseek-v4-flash` (the web app's \"Fast Mode\") or `deepseek-v4-pro` (the web app's \"Expert Mode\").\n\n## Step 4: Write the ByteMind Config\n\nReplace `YOUR_DEEPSEEK_API_KEY` with the key you just copied.\n\n\n\n\n```powershell\nNew-Item -ItemType Directory -Force \"$env:USERPROFILE\\.bytemind\" | Out-Null\n$config = @'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\n'@\n\n$utf8NoBom = New-Object System.Text.UTF8Encoding($false)\n[System.IO.File]::WriteAllText(\"$env:USERPROFILE\\.bytemind\\config.json\", $config, $utf8NoBom)\n```\n\nThis writes `config.json` as UTF-8 without BOM, so it works consistently in both Windows PowerShell 5.1 and PowerShell 7+.\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n## Step 5: Verify It Works\n\nEnter a specific project directory and start ByteMind:\n\n```bash\nbytemind\n```\n\nTry a very small task:\n\n```text\nDescribe this project in one sentence.\n```\n\nIf the model replies normally, your API key, base URL, and model ID are configured correctly.\n\n## FAQ\n\n**What should `provider.type` be?**\n\nDeepSeek uses the OpenAI-compatible format, so use `openai-compatible`.\n\n**Should `base_url` include `/v1`?**\n\nDeepSeek's official OpenAI-format base URL is `https://api.deepseek.com`. ByteMind appends the default `/chat/completions` path, so do not add `/chat/completions` yourself.\n\n**Can I use `api_key_env` instead of `api_key`?**\n\nYes, and it's safer. Replace `\"api_key\"` with `\"api_key_env\": \"DEEPSEEK_API_KEY\"`, then set the environment variable:\n\n\n\n\n```powershell\n# Temporary (current window only):\n$env:DEEPSEEK_API_KEY = \"sk-...\"\n\n# Permanent (survives reboots):\n[Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\", \"sk-...\", \"User\")\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n:::warning Don't set both `api_key` and `api_key_env`\nIf both are present, `api_key` takes priority and `api_key_env` is ignored. Use one or the other.\n:::\n\n**Can I use any model ID?**\n\nNo. The model ID must exactly match the name in the provider's documentation. For DeepSeek, start with `deepseek-v4-flash`; if you need stronger capability, switch to `deepseek-v4-pro` according to the official docs.\n\n**What if it still fails?**\n\nCheck three things first: whether the key was copied completely, whether your provider balance is usable, and whether `base_url` or `model` has any extra or missing characters. See [Troubleshooting](/troubleshooting) for more checks.\n"}, "a7ba49d11ff5d558fad92fab60ff6baa7786ee01": {"t": 1785749488.954284, "n": 5529, "h": "56f972205b652066", "c": "# Get API Key\n\nMany model providers offer an \"OpenAI-compatible API\". To configure ByteMind, collect these four values from your provider:\n\n| Info | ByteMind field | Example |\n| ---- | -------------- | ------- |\n| Provider type | `provider.type` | `openai-compatible` |\n| API endpoint | `provider.base_url` | `https://api.deepseek.com` |\n| Model ID | `provider.model` | `deepseek-v4-flash` |\n| API key | `provider.api_key` | `sk-...` |\n\nThis page uses DeepSeek as an example. Other OpenAI-compatible providers follow the same general flow: find the API key first, then find the base URL and model ID.\n\n## Step 1: Open the Provider Console\n\nOpen [DeepSeek Platform](https://platform.deepseek.com/api_keys), sign in, and go to the API Keys page.\n\n![DeepSeek API Keys page](./zh/assets/api_key1.png)\n\nIf you do not have usable balance, check your balance or top-up status first. API calls are usually billed by token usage. DeepSeek's official documentation says fees are deducted from topped-up or granted balance; prices may change, so use the [Models & Pricing](https://api-docs.deepseek.com/quick_start/pricing) page as the source of truth.\n\n## Step 2: Create an API Key\n\nCreate a new key on the API Keys page. After creation, copy the full key and store it somewhere safe on your machine. You will use it when configuring ByteMind.\n\n![DeepSeek API Keys page](./zh/assets/api_key2.png)\n\n![DeepSeek API Keys page](./zh/assets/api_key3.png)\n\nDo not share your API key with others, and do not commit it to a public repository. For beginners, writing it into your local `~/.bytemind/config.json` is the most direct option; once you are comfortable, you can switch to environment variables.\n\n## Step 3: Read the API Docs\n\nOpen DeepSeek's [First API Call](https://api-docs.deepseek.com/) documentation and focus on these fields:\n\n![DeepSeek API documentation](./zh/assets/deepseek_api.png)\n\n| Doc name | ByteMind config | DeepSeek example |\n| -------- | --------------- | ---------------- |\n| `base_url (OpenAI)` | `provider.base_url` | `https://api.deepseek.com` |\n| | `provider.type` | `openai-compatible` |\n| `api_key` | `provider.api_key` | The key you just created |\n| `model` | `provider.model` | `deepseek-v4-flash` |\n\nAs of 2026-05-07, DeepSeek's official docs recommend OpenAI-format models including `deepseek-v4-flash` and `deepseek-v4-pro`. The older model names `deepseek-chat` and `deepseek-reasoner` are marked for deprecation on 2026-07-24, so new configs should prefer `deepseek-v4-flash` (the web app's \"Fast Mode\") or `deepseek-v4-pro` (the web app's \"Expert Mode\").\n\n## Step 4: Write the ByteMind Config\n\nReplace `YOUR_DEEPSEEK_API_KEY` with the key you just copied.\n\n\n\n\n```powershell\nNew-Item -ItemType Directory -Force \"$env:USERPROFILE\\.bytemind\" | Out-Null\n@'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\n'@ | Set-Content -Encoding utf8 \"$env:USERPROFILE\\.bytemind\\config.json\"\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n## Step 5: Verify It Works\n\nEnter a specific project directory and start ByteMind:\n\n```bash\nbytemind\n```\n\nTry a very small task:\n\n```text\nDescribe this project in one sentence.\n```\n\nIf the model replies normally, your API key, base URL, and model ID are configured correctly.\n\n## FAQ\n\n**What should `provider.type` be?**\n\nDeepSeek uses the OpenAI-compatible format, so use `openai-compatible`.\n\n**Should `base_url` include `/v1`?**\n\nDeepSeek's official OpenAI-format base URL is `https://api.deepseek.com`. ByteMind appends the default `/chat/completions` path, so do not add `/chat/completions` yourself.\n\n**Can I use `api_key_env` instead of `api_key`?**\n\nYes, and it's safer. Replace `\"api_key\"` with `\"api_key_env\": \"DEEPSEEK_API_KEY\"`, then set the environment variable:\n\n\n\n\n```powershell\n# Temporary (current window only):\n$env:DEEPSEEK_API_KEY = \"sk-...\"\n\n# Permanent (survives reboots):\n[Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\", \"sk-...\", \"User\")\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n:::warning Don't set both `api_key` and `api_key_env`\nIf both are present, `api_key` takes priority and `api_key_env` is ignored. Use one or the other.\n:::\n\n**Can I use any model ID?**\n\nNo. The model ID must exactly match the name in the provider's documentation. For DeepSeek, start with `deepseek-v4-flash`; if you need stronger capability, switch to `deepseek-v4-pro` according to the official docs.\n\n**What if it still fails?**\n\nCheck three things first: whether the key was copied completely, whether your provider balance is usable, and whether `base_url` or `model` has any extra or missing characters. See [Troubleshooting](/troubleshooting) for more checks.\n"}, "b4926e67a8ab82e6e8fa0d92a38401ff303e504b": {"t": 1785749490.0154543, "n": 4782, "h": "271db2ccae5d4cdc", "c": "# Get API Key\n\nMany model providers offer an \"OpenAI-compatible API\". To configure ByteMind, collect these four values from your provider:\n\n| Info | ByteMind field | Example |\n| ---- | -------------- | ------- |\n| Provider type | `provider.type` | `openai-compatible` |\n| API endpoint | `provider.base_url` | `https://api.deepseek.com` |\n| Model ID | `provider.model` | `deepseek-v4-flash` |\n| API key | `provider.api_key` | `sk-...` |\n\nThis page uses DeepSeek as an example. Other OpenAI-compatible providers follow the same general flow: find the API key first, then find the base URL and model ID.\n\n## Step 1: Open the Provider Console\n\nOpen [DeepSeek Platform](https://platform.deepseek.com/api_keys), sign in, and go to the API Keys page.\n\n![DeepSeek API Keys page](./zh/assets/api_key1.png)\n\nIf you do not have usable balance, check your balance or top-up status first. API calls are usually billed by token usage. DeepSeek's official documentation says fees are deducted from topped-up or granted balance; prices may change, so use the [Models & Pricing](https://api-docs.deepseek.com/quick_start/pricing) page as the source of truth.\n\n## Step 2: Create an API Key\n\nCreate a new key on the API Keys page. After creation, copy the full key and store it somewhere safe on your machine. You will use it when configuring ByteMind.\n\n![DeepSeek API Keys page](./zh/assets/api_key2.png)\n\n![DeepSeek API Keys page](./zh/assets/api_key3.png)\n\nDo not share your API key with others, and do not commit it to a public repository. For beginners, writing it into your local `~/.bytemind/config.json` is the most direct option; once you are comfortable, you can switch to environment variables.\n\n## Step 3: Read the API Docs\n\nOpen DeepSeek's [First API Call](https://api-docs.deepseek.com/) documentation and focus on these fields:\n\n![DeepSeek API documentation](./zh/assets/deepseek_api.png)\n\n| Doc name | ByteMind config | DeepSeek example |\n| -------- | --------------- | ---------------- |\n| `base_url (OpenAI)` | `provider.base_url` | `https://api.deepseek.com` |\n| | `provider.type` | `openai-compatible` |\n| `api_key` | `provider.api_key` | The key you just created |\n| `model` | `provider.model` | `deepseek-v4-flash` |\n\nAs of 2026-05-07, DeepSeek's official docs recommend OpenAI-format models including `deepseek-v4-flash` and `deepseek-v4-pro`. The older model names `deepseek-chat` and `deepseek-reasoner` are marked for deprecation on 2026-07-24, so new configs should prefer `deepseek-v4-flash` (the web app's \"Fast Mode\") or `deepseek-v4-pro` (the web app's \"Expert Mode\").\n\n## Step 4: Write the ByteMind Config\n\nReplace `YOUR_DEEPSEEK_API_KEY` with the key you just copied.\n\n\n\n\n```powershell\nNew-Item -ItemType Directory -Force \"$env:USERPROFILE\\.bytemind\" | Out-Null\n@'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\n'@ | Set-Content -Encoding utf8 \"$env:USERPROFILE\\.bytemind\\config.json\"\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n## Step 5: Verify It Works\n\nEnter a specific project directory and start ByteMind:\n\n```bash\nbytemind\n```\n\nTry a very small task:\n\n```text\nDescribe this project in one sentence.\n```\n\nIf the model replies normally, your API key, base URL, and model ID are configured correctly.\n\n## FAQ\n\n**What should `provider.type` be?**\n\nDeepSeek uses the OpenAI-compatible format, so use `openai-compatible`.\n\n**Should `base_url` include `/v1`?**\n\nDeepSeek's official OpenAI-format base URL is `https://api.deepseek.com`. ByteMind appends the default `/chat/completions` path, so do not add `/chat/completions` yourself.\n\n**Can I use any model ID?**\n\nNo. The model ID must exactly match the name in the provider's documentation. For DeepSeek, start with `deepseek-v4-flash`; if you need stronger capability, switch to `deepseek-v4-pro` according to the official docs.\n\n**What if it still fails?**\n\nCheck three things first: whether the key was copied completely, whether your provider balance is usable, and whether `base_url` or `model` has any extra or missing characters. See [Troubleshooting](/troubleshooting) for more checks.\n"}}, "www/zh/api-key.md": {"f259496ed7f959b3400c57e2d3d4d0a548d4b6a3": {"t": 1785674069.9663787, "n": 4146, "h": "66bf65aa277a4bec", "c": "# 获取 API Key\n\n很多模型服务都提供“OpenAI 兼容 API”。你需要从服务商那里拿到四个信息,再填进 ByteMind 配置:\n\n| 信息 | 在 ByteMind 里的字段 | 例子 |\n| ---- | -------------------- | ---- |\n| 服务类型 | `provider.type` | `openai-compatible` |\n| API 地址 | `provider.base_url` | `https://api.deepseek.com` |\n| 模型 ID | `provider.model` | `deepseek-v4-flash` |\n| API Key | `provider.api_key` | `sk-...` |\n\n下面以 DeepSeek 为例说明。其他 OpenAI 兼容服务也基本按同样思路查找:先找 API Key,再找 Base URL 和模型 ID。\n\n## 第一步:进入服务商控制台\n\n打开 [DeepSeek Platform](https://platform.deepseek.com/api_keys),登录账号后进入 API Keys 页面。\n\n![DeepSeek API Keys 页面](./assets/api_key1.png)\n\n如果你还没有可用额度,先在平台里确认余额或充值状态。API 调用通常按 token 用量扣费;DeepSeek 官方说明费用会从充值余额或赠送余额中扣除,价格可能调整,实际以 [模型与价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing) 页面为准。\n\n## 第二步:创建 API Key\n\n在 API Keys 页面创建一个新的 Key。创建后复制完整密钥,保存到本机安全位置,后面配置 ByteMind 时会用到。\n\n![DeepSeek API Keys 页面](./assets/api_key2.png)\n\n![DeepSeek API Keys 页面](./assets/api_key3.png)\n\n不要把 API Key 发给别人,也不要提交到公开仓库。对新手来说,先写进本机 `~/.bytemind/config.json` 最直观;熟悉后可以改用环境变量。\n\n## 第三步:解读 API 文档\n\n打开 DeepSeek 的 [首次调用 API](https://api-docs.deepseek.com/zh-cn/) 文档,重点只看这几项:\n\n![DeepSeek API Keys 页面](./assets/deepseek_api.png)\n\n| 文档里的名字 | ByteMind 配置 | DeepSeek 示例 |\n| ------------ | ------------- | ------------- |\n| `base_url (OpenAI)` | `provider.base_url` | `https://api.deepseek.com` |\n| | `provider.type` | `openai-compatible` |\n| `api_key` | `provider.api_key` | 你刚创建的 Key |\n| `model` | `provider.model` | `deepseek-v4-flash` |\n\n截至 2026-05-07,DeepSeek 官方文档推荐的 OpenAI 格式模型包括 `deepseek-v4-flash` 和 `deepseek-v4-pro`。旧模型名 `deepseek-chat` 与 `deepseek-reasoner` 官方标注将于 2026-07-24 弃用,因此新配置建议优先使用 `deepseek-v4-flash`(对应网页版“快速模式”) 或 `deepseek-v4-pro`(对应网页版“专家模式”)。\n\n## 第四步:写入 ByteMind 配置\n\n把下面的 `YOUR_DEEPSEEK_API_KEY` 替换为你刚复制的 Key。\n\n\n\n\n```powershell\nNew-Item -ItemType Directory -Force \"$env:USERPROFILE\\.bytemind\" | Out-Null\n$config = @'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\n'@\n\n$utf8NoBom = New-Object System.Text.UTF8Encoding($false)\n[System.IO.File]::WriteAllText(\"$env:USERPROFILE\\.bytemind\\config.json\", $config, $utf8NoBom)\n```\n\n这会把 `config.json` 写成不带 BOM 的 UTF-8,在 Windows PowerShell 5.1 和 PowerShell 7+ 中都能稳定工作。\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n## 第五步:验证是否可用\n\n进入一个具体项目目录后启动 ByteMind:\n\n```bash\nbytemind\n```\n\n输入一个很小的任务,例如:\n\n```text\n用一句话介绍这个项目。\n```\n\n如果模型正常回复,说明 API Key、Base URL 和模型 ID 都配置成功。\n\n## 常见问题\n\n**`provider.type` 应该填什么?**\n\nDeepSeek 使用 OpenAI 兼容格式,所以填 `openai-compatible`。\n\n**`base_url` 要不要加 `/v1`?**\n\nDeepSeek 官方文档给出的 OpenAI 格式 Base URL 是 `https://api.deepseek.com`。ByteMind 会在后面拼接默认接口路径 `/chat/completions`,所以这里不要再加 `/chat/completions`。\n\n**可以用 `api_key_env` 代替 `api_key` 吗?**\n\n可以,而且更安全。把 `\"api_key\"` 替换为 `\"api_key_env\": \"DEEPSEEK_API_KEY\"`,然后设置环境变量:\n\n\n\n\n```powershell\n# 临时(仅当前窗口有效):\n$env:DEEPSEEK_API_KEY = \"sk-...\"\n\n# 永久(重启电脑后依然有效):\n[Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\", \"sk-...\", \"User\")\n# 执行后需重启终端窗口。\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n:::warning 不要同时设置 `api_key` 和 `api_key_env`\n如果两个都存在,`api_key` 优先,`api_key_env` 会被忽略。二选一即可。\n:::\n\n**模型 ID 可以随便写吗?**\n\n不可以。模型 ID 必须和服务商文档里的名字完全一致。DeepSeek 当前建议从 `deepseek-v4-flash` 开始;如果你需要更高能力,再按官方文档改成 `deepseek-v4-pro`。\n\n**仍然报错怎么办?**\n\n先检查三件事:Key 有没有复制完整、平台余额是否可用、`base_url` 和 `model` 有没有多写或少写字符。更多排查见[故障排查](/zh/troubleshooting)。\n"}, "0e4d543804070e03cb4f5675e90d2b2f6faf7f84": {"t": 1785749484.8980498, "n": 4146, "h": "66bf65aa277a4bec", "c": "# 获取 API Key\n\n很多模型服务都提供“OpenAI 兼容 API”。你需要从服务商那里拿到四个信息,再填进 ByteMind 配置:\n\n| 信息 | 在 ByteMind 里的字段 | 例子 |\n| ---- | -------------------- | ---- |\n| 服务类型 | `provider.type` | `openai-compatible` |\n| API 地址 | `provider.base_url` | `https://api.deepseek.com` |\n| 模型 ID | `provider.model` | `deepseek-v4-flash` |\n| API Key | `provider.api_key` | `sk-...` |\n\n下面以 DeepSeek 为例说明。其他 OpenAI 兼容服务也基本按同样思路查找:先找 API Key,再找 Base URL 和模型 ID。\n\n## 第一步:进入服务商控制台\n\n打开 [DeepSeek Platform](https://platform.deepseek.com/api_keys),登录账号后进入 API Keys 页面。\n\n![DeepSeek API Keys 页面](./assets/api_key1.png)\n\n如果你还没有可用额度,先在平台里确认余额或充值状态。API 调用通常按 token 用量扣费;DeepSeek 官方说明费用会从充值余额或赠送余额中扣除,价格可能调整,实际以 [模型与价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing) 页面为准。\n\n## 第二步:创建 API Key\n\n在 API Keys 页面创建一个新的 Key。创建后复制完整密钥,保存到本机安全位置,后面配置 ByteMind 时会用到。\n\n![DeepSeek API Keys 页面](./assets/api_key2.png)\n\n![DeepSeek API Keys 页面](./assets/api_key3.png)\n\n不要把 API Key 发给别人,也不要提交到公开仓库。对新手来说,先写进本机 `~/.bytemind/config.json` 最直观;熟悉后可以改用环境变量。\n\n## 第三步:解读 API 文档\n\n打开 DeepSeek 的 [首次调用 API](https://api-docs.deepseek.com/zh-cn/) 文档,重点只看这几项:\n\n![DeepSeek API Keys 页面](./assets/deepseek_api.png)\n\n| 文档里的名字 | ByteMind 配置 | DeepSeek 示例 |\n| ------------ | ------------- | ------------- |\n| `base_url (OpenAI)` | `provider.base_url` | `https://api.deepseek.com` |\n| | `provider.type` | `openai-compatible` |\n| `api_key` | `provider.api_key` | 你刚创建的 Key |\n| `model` | `provider.model` | `deepseek-v4-flash` |\n\n截至 2026-05-07,DeepSeek 官方文档推荐的 OpenAI 格式模型包括 `deepseek-v4-flash` 和 `deepseek-v4-pro`。旧模型名 `deepseek-chat` 与 `deepseek-reasoner` 官方标注将于 2026-07-24 弃用,因此新配置建议优先使用 `deepseek-v4-flash`(对应网页版“快速模式”) 或 `deepseek-v4-pro`(对应网页版“专家模式”)。\n\n## 第四步:写入 ByteMind 配置\n\n把下面的 `YOUR_DEEPSEEK_API_KEY` 替换为你刚复制的 Key。\n\n\n\n\n```powershell\nNew-Item -ItemType Directory -Force \"$env:USERPROFILE\\.bytemind\" | Out-Null\n$config = @'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\n'@\n\n$utf8NoBom = New-Object System.Text.UTF8Encoding($false)\n[System.IO.File]::WriteAllText(\"$env:USERPROFILE\\.bytemind\\config.json\", $config, $utf8NoBom)\n```\n\n这会把 `config.json` 写成不带 BOM 的 UTF-8,在 Windows PowerShell 5.1 和 PowerShell 7+ 中都能稳定工作。\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n## 第五步:验证是否可用\n\n进入一个具体项目目录后启动 ByteMind:\n\n```bash\nbytemind\n```\n\n输入一个很小的任务,例如:\n\n```text\n用一句话介绍这个项目。\n```\n\n如果模型正常回复,说明 API Key、Base URL 和模型 ID 都配置成功。\n\n## 常见问题\n\n**`provider.type` 应该填什么?**\n\nDeepSeek 使用 OpenAI 兼容格式,所以填 `openai-compatible`。\n\n**`base_url` 要不要加 `/v1`?**\n\nDeepSeek 官方文档给出的 OpenAI 格式 Base URL 是 `https://api.deepseek.com`。ByteMind 会在后面拼接默认接口路径 `/chat/completions`,所以这里不要再加 `/chat/completions`。\n\n**可以用 `api_key_env` 代替 `api_key` 吗?**\n\n可以,而且更安全。把 `\"api_key\"` 替换为 `\"api_key_env\": \"DEEPSEEK_API_KEY\"`,然后设置环境变量:\n\n\n\n\n```powershell\n# 临时(仅当前窗口有效):\n$env:DEEPSEEK_API_KEY = \"sk-...\"\n\n# 永久(重启电脑后依然有效):\n[Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\", \"sk-...\", \"User\")\n# 执行后需重启终端窗口。\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n:::warning 不要同时设置 `api_key` 和 `api_key_env`\n如果两个都存在,`api_key` 优先,`api_key_env` 会被忽略。二选一即可。\n:::\n\n**模型 ID 可以随便写吗?**\n\n不可以。模型 ID 必须和服务商文档里的名字完全一致。DeepSeek 当前建议从 `deepseek-v4-flash` 开始;如果你需要更高能力,再按官方文档改成 `deepseek-v4-pro`。\n\n**仍然报错怎么办?**\n\n先检查三件事:Key 有没有复制完整、平台余额是否可用、`base_url` 和 `model` 有没有多写或少写字符。更多排查见[故障排查](/zh/troubleshooting)。\n"}, "a7ba49d11ff5d558fad92fab60ff6baa7786ee01": {"t": 1785749486.5755758, "n": 3968, "h": "b1f708837de77961", "c": "# 获取 API Key\n\n很多模型服务都提供“OpenAI 兼容 API”。你需要从服务商那里拿到四个信息,再填进 ByteMind 配置:\n\n| 信息 | 在 ByteMind 里的字段 | 例子 |\n| ---- | -------------------- | ---- |\n| 服务类型 | `provider.type` | `openai-compatible` |\n| API 地址 | `provider.base_url` | `https://api.deepseek.com` |\n| 模型 ID | `provider.model` | `deepseek-v4-flash` |\n| API Key | `provider.api_key` | `sk-...` |\n\n下面以 DeepSeek 为例说明。其他 OpenAI 兼容服务也基本按同样思路查找:先找 API Key,再找 Base URL 和模型 ID。\n\n## 第一步:进入服务商控制台\n\n打开 [DeepSeek Platform](https://platform.deepseek.com/api_keys),登录账号后进入 API Keys 页面。\n\n![DeepSeek API Keys 页面](./assets/api_key1.png)\n\n如果你还没有可用额度,先在平台里确认余额或充值状态。API 调用通常按 token 用量扣费;DeepSeek 官方说明费用会从充值余额或赠送余额中扣除,价格可能调整,实际以 [模型与价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing) 页面为准。\n\n## 第二步:创建 API Key\n\n在 API Keys 页面创建一个新的 Key。创建后复制完整密钥,保存到本机安全位置,后面配置 ByteMind 时会用到。\n\n![DeepSeek API Keys 页面](./assets/api_key2.png)\n\n![DeepSeek API Keys 页面](./assets/api_key3.png)\n\n不要把 API Key 发给别人,也不要提交到公开仓库。对新手来说,先写进本机 `~/.bytemind/config.json` 最直观;熟悉后可以改用环境变量。\n\n## 第三步:解读 API 文档\n\n打开 DeepSeek 的 [首次调用 API](https://api-docs.deepseek.com/zh-cn/) 文档,重点只看这几项:\n\n![DeepSeek API Keys 页面](./assets/deepseek_api.png)\n\n| 文档里的名字 | ByteMind 配置 | DeepSeek 示例 |\n| ------------ | ------------- | ------------- |\n| `base_url (OpenAI)` | `provider.base_url` | `https://api.deepseek.com` |\n| | `provider.type` | `openai-compatible` |\n| `api_key` | `provider.api_key` | 你刚创建的 Key |\n| `model` | `provider.model` | `deepseek-v4-flash` |\n\n截至 2026-05-07,DeepSeek 官方文档推荐的 OpenAI 格式模型包括 `deepseek-v4-flash` 和 `deepseek-v4-pro`。旧模型名 `deepseek-chat` 与 `deepseek-reasoner` 官方标注将于 2026-07-24 弃用,因此新配置建议优先使用 `deepseek-v4-flash`(对应网页版“快速模式”) 或 `deepseek-v4-pro`(对应网页版“专家模式”)。\n\n## 第四步:写入 ByteMind 配置\n\n把下面的 `YOUR_DEEPSEEK_API_KEY` 替换为你刚复制的 Key。\n\n\n\n\n```powershell\nNew-Item -ItemType Directory -Force \"$env:USERPROFILE\\.bytemind\" | Out-Null\n@'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\n'@ | Set-Content -Encoding utf8 \"$env:USERPROFILE\\.bytemind\\config.json\"\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n## 第五步:验证是否可用\n\n进入一个具体项目目录后启动 ByteMind:\n\n```bash\nbytemind\n```\n\n输入一个很小的任务,例如:\n\n```text\n用一句话介绍这个项目。\n```\n\n如果模型正常回复,说明 API Key、Base URL 和模型 ID 都配置成功。\n\n## 常见问题\n\n**`provider.type` 应该填什么?**\n\nDeepSeek 使用 OpenAI 兼容格式,所以填 `openai-compatible`。\n\n**`base_url` 要不要加 `/v1`?**\n\nDeepSeek 官方文档给出的 OpenAI 格式 Base URL 是 `https://api.deepseek.com`。ByteMind 会在后面拼接默认接口路径 `/chat/completions`,所以这里不要再加 `/chat/completions`。\n\n**可以用 `api_key_env` 代替 `api_key` 吗?**\n\n可以,而且更安全。把 `\"api_key\"` 替换为 `\"api_key_env\": \"DEEPSEEK_API_KEY\"`,然后设置环境变量:\n\n\n\n\n```powershell\n# 临时(仅当前窗口有效):\n$env:DEEPSEEK_API_KEY = \"sk-...\"\n\n# 永久(重启电脑后依然有效):\n[Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\", \"sk-...\", \"User\")\n# 执行后需重启终端窗口。\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n\n\n\n:::warning 不要同时设置 `api_key` 和 `api_key_env`\n如果两个都存在,`api_key` 优先,`api_key_env` 会被忽略。二选一即可。\n:::\n\n**模型 ID 可以随便写吗?**\n\n不可以。模型 ID 必须和服务商文档里的名字完全一致。DeepSeek 当前建议从 `deepseek-v4-flash` 开始;如果你需要更高能力,再按官方文档改成 `deepseek-v4-pro`。\n\n**仍然报错怎么办?**\n\n先检查三件事:Key 有没有复制完整、平台余额是否可用、`base_url` 和 `model` 有没有多写或少写字符。更多排查见[故障排查](/zh/troubleshooting)。\n"}, "c903b023c3c8e13608207b36dddfdd15a32b8401": {"t": 1785749487.620199, "n": 3360, "h": "83d67c3002478ee5", "c": "# 获取 API Key\n\n很多模型服务都提供“OpenAI 兼容 API”。你需要从服务商那里拿到四个信息,再填进 ByteMind 配置:\n\n| 信息 | 在 ByteMind 里的字段 | 例子 |\n| ---- | -------------------- | ---- |\n| 服务类型 | `provider.type` | `openai-compatible` |\n| API 地址 | `provider.base_url` | `https://api.deepseek.com` |\n| 模型 ID | `provider.model` | `deepseek-v4-flash` |\n| API Key | `provider.api_key` | `sk-...` |\n\n下面以 DeepSeek 为例说明。其他 OpenAI 兼容服务也基本按同样思路查找:先找 API Key,再找 Base URL 和模型 ID。\n\n## 第一步:进入服务商控制台\n\n打开 [DeepSeek Platform](https://platform.deepseek.com/api_keys),登录账号后进入 API Keys 页面。\n\n![DeepSeek API Keys 页面](./assets/api_key1.png)\n\n如果你还没有可用额度,先在平台里确认余额或充值状态。API 调用通常按 token 用量扣费;DeepSeek 官方说明费用会从充值余额或赠送余额中扣除,价格可能调整,实际以 [模型与价格](https://api-docs.deepseek.com/zh-cn/quick_start/pricing) 页面为准。\n\n## 第二步:创建 API Key\n\n在 API Keys 页面创建一个新的 Key。创建后复制完整密钥,保存到本机安全位置,后面配置 ByteMind 时会用到。\n\n![DeepSeek API Keys 页面](./assets/api_key2.png)\n\n![DeepSeek API Keys 页面](./assets/api_key3.png)\n\n不要把 API Key 发给别人,也不要提交到公开仓库。对新手来说,先写进本机 `~/.bytemind/config.json` 最直观;熟悉后可以改用环境变量。\n\n## 第三步:解读 API 文档\n\n打开 DeepSeek 的 [首次调用 API](https://api-docs.deepseek.com/zh-cn/) 文档,重点只看这几项:\n\n![DeepSeek API Keys 页面](./assets/deepseek_api.png)\n\n| 文档里的名字 | ByteMind 配置 | DeepSeek 示例 |\n| ------------ | ------------- | ------------- |\n| `base_url (OpenAI)` | `provider.base_url` | `https://api.deepseek.com` |\n| | `provider.type` | `openai-compatible` |\n| `api_key` | `provider.api_key` | 你刚创建的 Key |\n| `model` | `provider.model` | `deepseek-v4-flash` |\n\n截至 2026-05-07,DeepSeek 官方文档推荐的 OpenAI 格式模型包括 `deepseek-v4-flash` 和 `deepseek-v4-pro`。旧模型名 `deepseek-chat` 与 `deepseek-reasoner` 官方标注将于 2026-07-24 弃用,因此新配置建议优先使用 `deepseek-v4-flash`(对应网页版“快速模式”) 或 `deepseek-v4-pro`(对应网页版“专家模式”)。\n\n## 第四步:写入 ByteMind 配置\n\n把下面的 `YOUR_DEEPSEEK_API_KEY` 替换为你刚复制的 Key。\n\n\n\n\n```powershell\nNew-Item -ItemType Directory -Force \"$env:USERPROFILE\\.bytemind\" | Out-Null\n@'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\n'@ | Set-Content -Encoding utf8 \"$env:USERPROFILE\\.bytemind\\config.json\"\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n\n```bash\nmkdir -p ~/.bytemind\ncat > ~/.bytemind/config.json <<'JSON'\n{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"model\": \"deepseek-v4-flash\",\n \"api_key\": \"YOUR_DEEPSEEK_API_KEY\"\n }\n}\nJSON\n```\n\n\n\n\n## 第五步:验证是否可用\n\n进入一个具体项目目录后启动 ByteMind:\n\n```bash\nbytemind\n```\n\n输入一个很小的任务,例如:\n\n```text\n用一句话介绍这个项目。\n```\n\n如果模型正常回复,说明 API Key、Base URL 和模型 ID 都配置成功。\n\n## 常见问题\n\n**`provider.type` 应该填什么?**\n\nDeepSeek 使用 OpenAI 兼容格式,所以填 `openai-compatible`。\n\n**`base_url` 要不要加 `/v1`?**\n\nDeepSeek 官方文档给出的 OpenAI 格式 Base URL 是 `https://api.deepseek.com`。ByteMind 会在后面拼接默认接口路径 `/chat/completions`,所以这里不要再加 `/chat/completions`。\n\n**模型 ID 可以随便写吗?**\n\n不可以。模型 ID 必须和服务商文档里的名字完全一致。DeepSeek 当前建议从 `deepseek-v4-flash` 开始;如果你需要更高能力,再按官方文档改成 `deepseek-v4-pro`。\n\n**仍然报错怎么办?**\n\n先检查三件事:Key 有没有复制完整、平台余额是否可用、`base_url` 和 `model` 有没有多写或少写字符。更多排查见[故障排查](/zh/troubleshooting)。\n"}}, "config.example.json": {"f259496ed7f959b3400c57e2d3d4d0a548d4b6a3": {"t": 1785674078.772056, "n": 1190, "h": "34e14f0594c646a1", "c": "{\n \"provider_runtime\": {\n \"current_provider\": \"deepseek\",\n \"default_provider\": \"deepseek\",\n \"default_model\": \"deepseek-v4-flash\",\n \"allow_fallback\": false,\n \"providers\": {\n \"deepseek\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"api_key_env\": \"DEEPSEEK_API_KEY\",\n \"model\": \"deepseek-v4-flash\",\n \"models\": [\n \"deepseek-v4-flash\",\n \"deepseek-v4-pro\"\n ]\n },\n \"openai\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"api_key_env\": \"OPENAI_API_KEY\",\n \"model\": \"gpt-5.4-mini\",\n \"models\": [\n \"gpt-5.4-mini\",\n \"gpt-5.4\"\n ]\n },\n \"mock\": {\n \"type\": \"mock\",\n \"model\": \"bugfix-demo\"\n }\n },\n \"health\": {\n \"fail_threshold\": 3,\n \"recover_probe_sec\": 30,\n \"recover_success_threshold\": 2,\n \"window_size\": 60\n }\n },\n \"approval_policy\": \"on-request\",\n \"approval_mode\": \"interactive\",\n \"max_iterations\": 64,\n \"stream\": true,\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n }\n}\n"}, "32aea9886130f13bd9174c1f903405d53454c8a8": {"t": 1785749486.8529625, "n": 1190, "h": "34e14f0594c646a1", "c": "{\n \"provider_runtime\": {\n \"current_provider\": \"deepseek\",\n \"default_provider\": \"deepseek\",\n \"default_model\": \"deepseek-v4-flash\",\n \"allow_fallback\": false,\n \"providers\": {\n \"deepseek\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"api_key_env\": \"DEEPSEEK_API_KEY\",\n \"model\": \"deepseek-v4-flash\",\n \"models\": [\n \"deepseek-v4-flash\",\n \"deepseek-v4-pro\"\n ]\n },\n \"openai\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"api_key_env\": \"OPENAI_API_KEY\",\n \"model\": \"gpt-5.4-mini\",\n \"models\": [\n \"gpt-5.4-mini\",\n \"gpt-5.4\"\n ]\n },\n \"mock\": {\n \"type\": \"mock\",\n \"model\": \"bugfix-demo\"\n }\n },\n \"health\": {\n \"fail_threshold\": 3,\n \"recover_probe_sec\": 30,\n \"recover_success_threshold\": 2,\n \"window_size\": 60\n }\n },\n \"approval_policy\": \"on-request\",\n \"approval_mode\": \"interactive\",\n \"max_iterations\": 64,\n \"stream\": true,\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n }\n}\n"}, "0a02782aac8d5f870967491bb68523d1bbc50fdf": {"t": 1785749487.9193146, "n": 1190, "h": "3a904e19d76e1b92", "c": "{\n \"provider_runtime\": {\n \"current_provider\": \"deepseek\",\n \"default_provider\": \"deepseek\",\n \"default_model\": \"deepseek-v4-flash\",\n \"allow_fallback\": false,\n \"providers\": {\n \"deepseek\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"api_key_env\": \"DEEPSEEK_API_KEY\",\n \"model\": \"deepseek-v4-flash\",\n \"models\": [\n \"deepseek-v4-flash\",\n \"deepseek-v4-pro\"\n ]\n },\n \"openai\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"api_key_env\": \"OPENAI_API_KEY\",\n \"model\": \"gpt-5.4-mini\",\n \"models\": [\n \"gpt-5.4-mini\",\n \"gpt-5.4\"\n ]\n },\n \"mock\": {\n \"type\": \"mock\",\n \"model\": \"bugfix-demo\"\n }\n },\n \"health\": {\n \"fail_threshold\": 3,\n \"recover_probe_sec\": 30,\n \"recover_success_threshold\": 2,\n \"window_size\": 60\n }\n },\n \"approval_policy\": \"on-request\",\n \"approval_mode\": \"interactive\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n }\n}\n"}, "e941ba15533b48d1f6f68ce7c272b38643ebd1bb": {"t": 1785749488.9863198, "n": 1110, "h": "248270d1ca47b2eb", "c": "{\n \"provider_runtime\": {\n \"current_provider\": \"deepseek\",\n \"default_provider\": \"deepseek\",\n \"default_model\": \"deepseek-v4-flash\",\n \"allow_fallback\": false,\n \"providers\": {\n \"deepseek\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"api_key_env\": \"DEEPSEEK_API_KEY\",\n \"model\": \"deepseek-v4-flash\",\n \"models\": [\n \"deepseek-v4-flash\",\n \"deepseek-v4-pro\"\n ]\n },\n \"openai\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"api_key_env\": \"OPENAI_API_KEY\",\n \"model\": \"gpt-5.4-mini\",\n \"models\": [\n \"gpt-5.4-mini\",\n \"gpt-5.4\"\n ]\n }\n },\n \"health\": {\n \"fail_threshold\": 3,\n \"recover_probe_sec\": 30,\n \"recover_success_threshold\": 2,\n \"window_size\": 60\n }\n },\n \"approval_policy\": \"on-request\",\n \"approval_mode\": \"interactive\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n }\n}\n"}, "917d34a0c8a8b72282af1a2fdd22c13ad1efe909": {"t": 1785749490.2009175, "n": 1171, "h": "65f4c6c5d19bc17b", "c": "{\n \"provider_runtime\": {\n \"current_provider\": \"deepseek\",\n \"providers\": {\n \"deepseek\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.deepseek.com\",\n \"api_key_env\": \"DEEPSEEK_API_KEY\",\n \"model\": \"deepseek-v4-flash\",\n \"models\": [\n \"deepseek-v4-flash\",\n \"deepseek-v4-pro\"\n ]\n },\n \"openai\": {\n \"type\": \"openai-compatible\",\n \"base_url\": \"https://api.openai.com/v1\",\n \"api_key_env\": \"OPENAI_API_KEY\",\n \"model\": \"gpt-5.4-mini\",\n \"models\": [\n \"gpt-5.4-mini\",\n \"gpt-5.4\"\n ]\n }\n }\n },\n \"approval_policy\": \"on-request\",\n \"approval_mode\": \"interactive\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n },\n \"token_usage\": {\n \"storage_type\": \"file\",\n \"storage_path\": \"./.bytemind/token_usage.json\",\n \"backup_interval\": \"1m\",\n \"max_sessions\": 10000,\n \"alert_threshold\": 1000000,\n \"enable_realtime\": true,\n \"retention_days\": 30,\n \"monitor_interval\": \"30s\",\n \"database_driver\": \"sqlite3\"\n }\n}\n"}, "d5153bf80454f29d2c6af00ceffbbb978ff9f5c5": {"t": 1785749491.4391563, "n": 952, "h": "008f15a4213eaa0f", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"notifications\": {\n \"desktop\": {\n \"enabled\": true,\n \"on_approval_required\": true,\n \"on_run_completed\": true,\n \"on_run_failed\": true,\n \"on_run_canceled\": false,\n \"cooldown_seconds\": 3\n }\n },\n \"max_iterations\": 32,\n \"stream\": true,\n \"update_check\": {\n \"enabled\": true\n },\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n },\n \"token_usage\": {\n \"storage_type\": \"file\",\n \"storage_path\": \"./.bytemind/token_usage.json\",\n \"backup_interval\": \"1m\",\n \"max_sessions\": 10000,\n \"alert_threshold\": 1000000,\n \"enable_realtime\": true,\n \"retention_days\": 30,\n \"monitor_interval\": \"30s\",\n \"database_driver\": \"sqlite3\"\n }\n}\n"}, "bfff580e98f486eeb8d58d9bc13fd6e4279a45e5": {"t": 1785749492.5885744, "n": 723, "h": "56119322e6da8c7a", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"update_check\": {\n \"enabled\": true\n },\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n },\n \"token_usage\": {\n \"storage_type\": \"file\",\n \"storage_path\": \"./.bytemind/token_usage.json\",\n \"backup_interval\": \"1m\",\n \"max_sessions\": 10000,\n \"alert_threshold\": 1000000,\n \"enable_realtime\": true,\n \"retention_days\": 30,\n \"monitor_interval\": \"30s\",\n \"database_driver\": \"sqlite3\"\n }\n}\n"}, "47936a746149554a088137082f42157b2512d909": {"t": 1785749493.8154888, "n": 1217, "h": "824145d450d91702", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"update_check\": {\n \"enabled\": true\n },\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n },\n \"token_usage\": {\n \"storage_type\": \"file\",\n \"storage_path\": \"./.bytemind/token_usage.json\",\n \"backup_interval\": \"1m\",\n \"max_sessions\": 10000,\n \"alert_threshold\": 1000000,\n \"enable_realtime\": true,\n \"retention_days\": 30,\n \"monitor_interval\": \"30s\",\n \"database_driver\": \"sqlite3\"\n },\n \"mcp\": {\n \"enabled\": false,\n \"sync_ttl_s\": 30,\n \"servers\": [\n {\n \"id\": \"local-mcp\",\n \"name\": \"Local MCP\",\n \"enabled\": true,\n \"transport\": {\n \"type\": \"stdio\",\n \"command\": \"node\",\n \"args\": [\n \"./mcp-server.js\"\n ],\n \"env\": {},\n \"cwd\": \"\"\n },\n \"auto_start\": true,\n \"startup_timeout_s\": 20,\n \"call_timeout_s\": 60,\n \"max_concurrency\": 4\n }\n ]\n }\n}\n"}, "d7c307b765507451dc188c93847de867860dbc3f": {"t": 1785749494.9002137, "n": 1485, "h": "f032ea7cf4830924", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"update_check\": {\n \"enabled\": true\n },\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n },\n \"extensions\": {\n \"sources\": [\n \"skills\",\n \"mcp\"\n ],\n \"auto_load\": true,\n \"health_check_interval_sec\": 30,\n \"failure_threshold\": 3,\n \"recovery_cooldown_sec\": 30,\n \"max_concurrency_per_extension\": 4,\n \"conflict_policy\": \"reject\"\n },\n \"token_usage\": {\n \"storage_type\": \"file\",\n \"storage_path\": \"./.bytemind/token_usage.json\",\n \"backup_interval\": \"1m\",\n \"max_sessions\": 10000,\n \"alert_threshold\": 1000000,\n \"enable_realtime\": true,\n \"retention_days\": 30,\n \"monitor_interval\": \"30s\",\n \"database_driver\": \"sqlite3\"\n },\n \"mcp\": {\n \"enabled\": false,\n \"sync_ttl_s\": 30,\n \"servers\": [\n {\n \"id\": \"local-mcp\",\n \"name\": \"Local MCP\",\n \"enabled\": true,\n \"transport\": {\n \"type\": \"stdio\",\n \"command\": \"node\",\n \"args\": [\n \"./mcp-server.js\"\n ],\n \"env\": {},\n \"cwd\": \"\"\n },\n \"auto_start\": true,\n \"startup_timeout_s\": 20,\n \"call_timeout_s\": 60,\n \"max_concurrency\": 4\n }\n ]\n }\n}\n"}, "ef90aa9a985769c18a14bb2fe1ea0af6edbf7dfb": {"t": 1785749496.1316044, "n": 1217, "h": "824145d450d91702", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"update_check\": {\n \"enabled\": true\n },\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n },\n \"token_usage\": {\n \"storage_type\": \"file\",\n \"storage_path\": \"./.bytemind/token_usage.json\",\n \"backup_interval\": \"1m\",\n \"max_sessions\": 10000,\n \"alert_threshold\": 1000000,\n \"enable_realtime\": true,\n \"retention_days\": 30,\n \"monitor_interval\": \"30s\",\n \"database_driver\": \"sqlite3\"\n },\n \"mcp\": {\n \"enabled\": false,\n \"sync_ttl_s\": 30,\n \"servers\": [\n {\n \"id\": \"local-mcp\",\n \"name\": \"Local MCP\",\n \"enabled\": true,\n \"transport\": {\n \"type\": \"stdio\",\n \"command\": \"node\",\n \"args\": [\n \"./mcp-server.js\"\n ],\n \"env\": {},\n \"cwd\": \"\"\n },\n \"auto_start\": true,\n \"startup_timeout_s\": 20,\n \"call_timeout_s\": 60,\n \"max_concurrency\": 4\n }\n ]\n }\n}\n"}, "56d5d49403af1093e6e360cd659dd5c502d04ed1": {"t": 1785749497.535781, "n": 723, "h": "56119322e6da8c7a", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"update_check\": {\n \"enabled\": true\n },\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n },\n \"token_usage\": {\n \"storage_type\": \"file\",\n \"storage_path\": \"./.bytemind/token_usage.json\",\n \"backup_interval\": \"1m\",\n \"max_sessions\": 10000,\n \"alert_threshold\": 1000000,\n \"enable_realtime\": true,\n \"retention_days\": 30,\n \"monitor_interval\": \"30s\",\n \"database_driver\": \"sqlite3\"\n }\n}\n"}, "4efefe52f6ebb242a2897901e1caba9591a5dfda": {"t": 1785749499.746147, "n": 678, "h": "18652b8bd4394028", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"context_budget\": {\n \"warning_ratio\": 0.85,\n \"critical_ratio\": 0.95,\n \"max_reactive_retry\": 1\n },\n \"token_usage\": {\n \"storage_type\": \"file\",\n \"storage_path\": \"./.bytemind/token_usage.json\",\n \"backup_interval\": \"1m\",\n \"max_sessions\": 10000,\n \"alert_threshold\": 1000000,\n \"enable_realtime\": true,\n \"retention_days\": 30,\n \"monitor_interval\": \"30s\",\n \"database_driver\": \"sqlite3\"\n }\n}\n"}, "ffb6a853f95b93ca916c24be7386a13ba733ad8a": {"t": 1785749500.8210511, "n": 568, "h": "a25d202bdf93b943", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"stream\": true,\n \"token_usage\": {\n \"storage_type\": \"file\",\n \"storage_path\": \"./.bytemind/token_usage.json\",\n \"backup_interval\": \"1m\",\n \"max_sessions\": 10000,\n \"alert_threshold\": 1000000,\n \"enable_realtime\": true,\n \"retention_days\": 30,\n \"monitor_interval\": \"30s\",\n \"database_driver\": \"sqlite3\"\n }\n}\n"}, "53a7019f3405ddc727fd75682ed0581e0d231ec0": {"t": 1785749502.0216112, "n": 257, "h": "7d5c63fee8dc66e2", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"stream\": true\n}\n"}, "5d0b8ca8c01a9246002f6f38c0d2ff6993c43342": {"t": 1785749503.2383764, "n": 278, "h": "ad27c9faca38e17a", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"session_dir\": \"\",\n \"stream\": true\n}\n"}, "41c6e9370caeb5b33e14ebc237c1afdb806e3973": {"t": 1785749505.1070926, "n": 193, "h": "b08e0fc6ff5ecc72", "c": "{\n \"provider\": {\n \"type\": \"\",\n \"base_url\": \"\",\n \"model\": \"\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"session_dir\": \"\",\n \"stream\": true\n}\n"}, "55046f5fbcf14113c9c00ce58fc50418d85d0c55": {"t": 1785749506.1765823, "n": 296, "h": "fd228657ab850f22", "c": "{\n \"provider\": {\n \"type\": \"openai-compatible\",\n \"auto_detect_type\": false,\n \"base_url\": \"https://api.openai.com/v1\",\n \"model\": \"gpt-5.4-mini\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"session_dir\": \".bytemind/sessions\",\n \"stream\": true\n}\n"}, "69ebf332a1ac327ca88545e0840ef4d836dca8c3": {"t": 1785749507.249594, "n": 211, "h": "2a1bb5fbb26ea6cf", "c": "{\n \"provider\": {\n \"type\": \"\",\n \"base_url\": \"\",\n \"model\": \"\",\n \"api_key\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"session_dir\": \".bytemind/sessions\",\n \"stream\": true\n}\n"}, "204a675c0e36005bed695f05dbbb021b5df70e5c": {"t": 1785749508.3159282, "n": 234, "h": "ae2eb257f3eb0c6d", "c": "{\n \"provider\": {\n \"type\": \"\",\n \"base_url\": \"\",\n \"model\": \"\",\n \"api_key\": \"\",\n \"api_key_env\": \"\"\n },\n \"approval_policy\": \"on-request\",\n \"max_iterations\": 32,\n \"session_dir\": \".bytemind/sessions\",\n \"stream\": true\n}\n"}}, "internal/app/cli_doctor.go": {"f259496ed7f959b3400c57e2d3d4d0a548d4b6a3": {"t": 1785745619.764574, "n": 3874, "h": "1d5157264c915c70", "c": "package app\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\tconfigpkg \"github.com/1024XEngineer/bytemind/internal/config\"\n\ttoolspkg \"github.com/1024XEngineer/bytemind/internal/tools\"\n)\n\nfunc RunDoctor(args []string, stdout, stderr io.Writer) error {\n\tworkspace := \".\"\n\tconfigFile := \"\"\n\tfor i := 0; i < len(args); i++ {\n\t\tswitch args[i] {\n\t\tcase \"-workspace\", \"--workspace\":\n\t\t\tif i+1 < len(args) {\n\t\t\t\tworkspace = args[i+1]\n\t\t\t\ti++\n\t\t\t}\n\t\tcase \"-config\", \"--config\":\n\t\t\tif i+1 < len(args) {\n\t\t\t\tconfigFile = args[i+1]\n\t\t\t\ti++\n\t\t\t}\n\t\t}\n\t}\n\n\twrite := func(format string, v ...any) {\n\t\tfmt.Fprintf(stdout, format+\"\\n\", v...)\n\t}\n\tpass := func(msg string) {\n\t\twrite(\" \\u2705 %s\", msg)\n\t}\n\tfail := func(msg string) {\n\t\twrite(\" \\u274c %s\", msg)\n\t}\n\twarn := func(msg string) {\n\t\twrite(\" \\u26a0\\ufe0f %s\", msg)\n\t}\n\n\twrite(\"ByteMind Doctor\")\n\twrite(\"\")\n\n\twrite(\"Environment:\")\n\tpass(fmt.Sprintf(\"Go %s\", runtime.Version()))\n\tpass(fmt.Sprintf(\"OS: %s / Arch: %s\", runtime.GOOS, runtime.GOARCH))\n\tif _, err := exec.LookPath(\"go\"); err == nil {\n\t\tpass(\"Go available\")\n\t} else {\n\t\twarn(\"Go not in PATH\")\n\t}\n\tif _, err := exec.LookPath(\"git\"); err == nil {\n\t\tpass(\"Git available\")\n\t} else {\n\t\twarn(\"Git not in PATH\")\n\t}\n\n\twrite(\"Workspace:\")\n\tabsWorkspace, _ := filepath.Abs(workspace)\n\tif info, err := os.Stat(absWorkspace); err == nil && info.IsDir() {\n\t\tpass(fmt.Sprintf(\"Valid workspace: %s\", absWorkspace))\n\t} else {\n\t\tfail(fmt.Sprintf(\"Workspace not found: %s\", absWorkspace))\n\t}\n\tif _, err := os.Stat(filepath.Join(absWorkspace, \".git\")); err == nil {\n\t\tpass(\"Git repository detected\")\n\t} else {\n\t\twarn(\"Not a git repository\")\n\t}\n\n\twrite(\"Configuration:\")\n\tcfg, err := configpkg.Load(workspace, configFile)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\twarn(fmt.Sprintf(\"No config file (optional): %v\", err))\n\t\t} else {\n\t\t\tfail(fmt.Sprintf(\"Config error: %v\", err))\n\t\t}\n\t\tcfg = configpkg.Default(workspace)\n\t} else {\n\t\tpass(\"Config file loaded\")\n\t}\n\n\twrite(\"Provider:\")\n\tselected := configpkg.SelectedModelID(cfg.ProviderRuntime)\n\tif selected != \"\" {\n\t\tpass(fmt.Sprintf(\"Model configured: %s\", selected))\n\t} else if cfg.Provider.Model != \"\" {\n\t\tpass(fmt.Sprintf(\"Model configured: %s\", cfg.Provider.Model))\n\t} else {\n\t\twarn(\"No model configured\")\n\t}\n\tkey := cfg.Provider.APIKey\n\tkeySource := \"\"\n\tif key != \"\" {\n\t\tkeySource = \"config file\"\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"ANTHROPIC_API_KEY\")\n\t\tif key != \"\" {\n\t\t\tkeySource = \"ANTHROPIC_API_KEY\"\n\t\t}\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"OPENAI_API_KEY\")\n\t\tif key != \"\" {\n\t\t\tkeySource = \"OPENAI_API_KEY\"\n\t\t}\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"DEEPSEEK_API_KEY\")\n\t\tif key != \"\" {\n\t\t\tkeySource = \"DEEPSEEK_API_KEY\"\n\t\t}\n\t}\n\tif key != \"\" {\n\t\tpass(fmt.Sprintf(\"API key found (%s)\", keySource))\n\t} else {\n\t\tfail(\"No API key found (set ANTHROPIC_API_KEY, OPENAI_API_KEY, or DEEPSEEK_API_KEY)\")\n\t}\n\n\twrite(\"Security:\")\n\tmode, _ := configpkg.NormalizeApprovalMode(cfg.ApprovalMode)\n\tpass(fmt.Sprintf(\"Approval mode: %s\", mode))\n\tpass(fmt.Sprintf(\"Approval policy: %s\", cfg.ApprovalPolicy))\n\tif cfg.SandboxEnabled {\n\t\tpass(fmt.Sprintf(\"Sandbox: enabled (%s)\", cfg.SystemSandboxMode))\n\t} else {\n\t\twarn(\"Sandbox: disabled\")\n\t}\n\tif len(cfg.WritableRoots) > 0 {\n\t\tpass(fmt.Sprintf(\"Writable roots: %s\", strings.Join(cfg.WritableRoots, \", \")))\n\t} else {\n\t\twarn(\"No writable roots (defaults to workspace only)\")\n\t}\n\tif len(cfg.ExecAllowlist) > 0 {\n\t\tpass(fmt.Sprintf(\"Exec allowlist: %d rule(s)\", len(cfg.ExecAllowlist)))\n\t} else {\n\t\twarn(\"No exec allowlist (all shell commands require approval)\")\n\t}\n\n\twrite(\"Tools:\")\n\treg := toolspkg.DefaultRegistry()\n\tpass(fmt.Sprintf(\"%d tools registered\", reg.Count()))\n\n\twrite(\"\")\n\twrite(\"Doctor check complete.\")\n\tif key == \"\" {\n\t\twrite(\"Tip: Set your API key via environment variable or config.json before using ByteMind.\")\n\t}\n\treturn nil\n}\n"}, "0a02782aac8d5f870967491bb68523d1bbc50fdf": {"t": 1785749484.367339, "n": 3874, "h": "1d5157264c915c70", "c": "package app\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\tconfigpkg \"github.com/1024XEngineer/bytemind/internal/config\"\n\ttoolspkg \"github.com/1024XEngineer/bytemind/internal/tools\"\n)\n\nfunc RunDoctor(args []string, stdout, stderr io.Writer) error {\n\tworkspace := \".\"\n\tconfigFile := \"\"\n\tfor i := 0; i < len(args); i++ {\n\t\tswitch args[i] {\n\t\tcase \"-workspace\", \"--workspace\":\n\t\t\tif i+1 < len(args) {\n\t\t\t\tworkspace = args[i+1]\n\t\t\t\ti++\n\t\t\t}\n\t\tcase \"-config\", \"--config\":\n\t\t\tif i+1 < len(args) {\n\t\t\t\tconfigFile = args[i+1]\n\t\t\t\ti++\n\t\t\t}\n\t\t}\n\t}\n\n\twrite := func(format string, v ...any) {\n\t\tfmt.Fprintf(stdout, format+\"\\n\", v...)\n\t}\n\tpass := func(msg string) {\n\t\twrite(\" \\u2705 %s\", msg)\n\t}\n\tfail := func(msg string) {\n\t\twrite(\" \\u274c %s\", msg)\n\t}\n\twarn := func(msg string) {\n\t\twrite(\" \\u26a0\\ufe0f %s\", msg)\n\t}\n\n\twrite(\"ByteMind Doctor\")\n\twrite(\"\")\n\n\twrite(\"Environment:\")\n\tpass(fmt.Sprintf(\"Go %s\", runtime.Version()))\n\tpass(fmt.Sprintf(\"OS: %s / Arch: %s\", runtime.GOOS, runtime.GOARCH))\n\tif _, err := exec.LookPath(\"go\"); err == nil {\n\t\tpass(\"Go available\")\n\t} else {\n\t\twarn(\"Go not in PATH\")\n\t}\n\tif _, err := exec.LookPath(\"git\"); err == nil {\n\t\tpass(\"Git available\")\n\t} else {\n\t\twarn(\"Git not in PATH\")\n\t}\n\n\twrite(\"Workspace:\")\n\tabsWorkspace, _ := filepath.Abs(workspace)\n\tif info, err := os.Stat(absWorkspace); err == nil && info.IsDir() {\n\t\tpass(fmt.Sprintf(\"Valid workspace: %s\", absWorkspace))\n\t} else {\n\t\tfail(fmt.Sprintf(\"Workspace not found: %s\", absWorkspace))\n\t}\n\tif _, err := os.Stat(filepath.Join(absWorkspace, \".git\")); err == nil {\n\t\tpass(\"Git repository detected\")\n\t} else {\n\t\twarn(\"Not a git repository\")\n\t}\n\n\twrite(\"Configuration:\")\n\tcfg, err := configpkg.Load(workspace, configFile)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\twarn(fmt.Sprintf(\"No config file (optional): %v\", err))\n\t\t} else {\n\t\t\tfail(fmt.Sprintf(\"Config error: %v\", err))\n\t\t}\n\t\tcfg = configpkg.Default(workspace)\n\t} else {\n\t\tpass(\"Config file loaded\")\n\t}\n\n\twrite(\"Provider:\")\n\tselected := configpkg.SelectedModelID(cfg.ProviderRuntime)\n\tif selected != \"\" {\n\t\tpass(fmt.Sprintf(\"Model configured: %s\", selected))\n\t} else if cfg.Provider.Model != \"\" {\n\t\tpass(fmt.Sprintf(\"Model configured: %s\", cfg.Provider.Model))\n\t} else {\n\t\twarn(\"No model configured\")\n\t}\n\tkey := cfg.Provider.APIKey\n\tkeySource := \"\"\n\tif key != \"\" {\n\t\tkeySource = \"config file\"\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"ANTHROPIC_API_KEY\")\n\t\tif key != \"\" {\n\t\t\tkeySource = \"ANTHROPIC_API_KEY\"\n\t\t}\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"OPENAI_API_KEY\")\n\t\tif key != \"\" {\n\t\t\tkeySource = \"OPENAI_API_KEY\"\n\t\t}\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"DEEPSEEK_API_KEY\")\n\t\tif key != \"\" {\n\t\t\tkeySource = \"DEEPSEEK_API_KEY\"\n\t\t}\n\t}\n\tif key != \"\" {\n\t\tpass(fmt.Sprintf(\"API key found (%s)\", keySource))\n\t} else {\n\t\tfail(\"No API key found (set ANTHROPIC_API_KEY, OPENAI_API_KEY, or DEEPSEEK_API_KEY)\")\n\t}\n\n\twrite(\"Security:\")\n\tmode, _ := configpkg.NormalizeApprovalMode(cfg.ApprovalMode)\n\tpass(fmt.Sprintf(\"Approval mode: %s\", mode))\n\tpass(fmt.Sprintf(\"Approval policy: %s\", cfg.ApprovalPolicy))\n\tif cfg.SandboxEnabled {\n\t\tpass(fmt.Sprintf(\"Sandbox: enabled (%s)\", cfg.SystemSandboxMode))\n\t} else {\n\t\twarn(\"Sandbox: disabled\")\n\t}\n\tif len(cfg.WritableRoots) > 0 {\n\t\tpass(fmt.Sprintf(\"Writable roots: %s\", strings.Join(cfg.WritableRoots, \", \")))\n\t} else {\n\t\twarn(\"No writable roots (defaults to workspace only)\")\n\t}\n\tif len(cfg.ExecAllowlist) > 0 {\n\t\tpass(fmt.Sprintf(\"Exec allowlist: %d rule(s)\", len(cfg.ExecAllowlist)))\n\t} else {\n\t\twarn(\"No exec allowlist (all shell commands require approval)\")\n\t}\n\n\twrite(\"Tools:\")\n\treg := toolspkg.DefaultRegistry()\n\tpass(fmt.Sprintf(\"%d tools registered\", reg.Count()))\n\n\twrite(\"\")\n\twrite(\"Doctor check complete.\")\n\tif key == \"\" {\n\t\twrite(\"Tip: Set your API key via environment variable or config.json before using ByteMind.\")\n\t}\n\treturn nil\n}\n"}, "6732ecc7ffc6a9a7b71db9d302b3a2e4980cb070": {"t": 1785749485.4175217, "n": 3549, "h": "ce9c4f831be4ee2d", "c": "package app\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\tconfigpkg \"github.com/1024XEngineer/bytemind/internal/config\"\n)\n\nfunc RunDoctor(args []string, stdout, stderr io.Writer) error {\n\tworkspace := \".\"\n\tconfigFile := \"\"\n\tfor i := 0; i < len(args); i++ {\n\t\tswitch args[i] {\n\t\tcase \"-workspace\", \"--workspace\":\n\t\t\tif i+1 < len(args) {\n\t\t\t\tworkspace = args[i+1]\n\t\t\t\ti++\n\t\t\t}\n\t\tcase \"-config\", \"--config\":\n\t\t\tif i+1 < len(args) {\n\t\t\t\tconfigFile = args[i+1]\n\t\t\t\ti++\n\t\t\t}\n\t\t}\n\t}\n\n\twrite := func(format string, v ...any) {\n\t\tfmt.Fprintf(stdout, format+\"\\n\", v...)\n\t}\n\tpass := func(msg string) {\n\t\twrite(\" [PASS] %s\", msg)\n\t}\n\tfail := func(msg string) {\n\t\twrite(\" [FAIL] %s\", msg)\n\t}\n\twarn := func(msg string) {\n\t\twrite(\" [WARN] %s\", msg)\n\t}\n\n\twrite(\"ByteMind Doctor\")\n\twrite(\"\")\n\n\twrite(\"Configuration:\")\n\tcfg, err := configpkg.Load(workspace, configFile)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\twarn(fmt.Sprintf(\"no config file found (optional): %v\", err))\n\t\t} else {\n\t\t\tfail(fmt.Sprintf(\"config file error: %v\", err))\n\t\t}\n\t\tcfg = configpkg.Default(workspace)\n\t} else {\n\t\tpass(\"config file loaded\")\n\t}\n\n\twrite(\"Provider:\")\n\tselected := configpkg.SelectedModelID(cfg.ProviderRuntime)\n\tif selected != \"\" {\n\t\tpass(fmt.Sprintf(\"model configured: %s\", selected))\n\t} else if cfg.Provider.Model != \"\" {\n\t\tpass(fmt.Sprintf(\"model configured: %s\", cfg.Provider.Model))\n\t} else {\n\t\twarn(\"no model configured\")\n\t}\n\tkey := cfg.Provider.APIKey\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"ANTHROPIC_API_KEY\")\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"OPENAI_API_KEY\")\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"DEEPSEEK_API_KEY\")\n\t}\n\tif key != \"\" {\n\t\tpass(\"API key detected\")\n\t} else {\n\t\tfail(\"no API key found (set ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY)\")\n\t}\n\n\twrite(\"Workspace:\")\n\tabsWorkspace, _ := filepath.Abs(workspace)\n\tif info, err := os.Stat(absWorkspace); err == nil && info.IsDir() {\n\t\tpass(fmt.Sprintf(\"workspace valid: %s\", absWorkspace))\n\t} else {\n\t\tfail(fmt.Sprintf(\"workspace not found: %s\", absWorkspace))\n\t}\n\tif _, err := exec.LookPath(\"git\"); err == nil {\n\t\tpass(\"git found in PATH\")\n\t} else {\n\t\twarn(\"git not found in PATH\")\n\t}\n\tif _, err := os.Stat(filepath.Join(absWorkspace, \".git\")); err == nil {\n\t\tpass(\"git repository detected\")\n\t}\n\tif _, err := os.Stat(filepath.Join(absWorkspace, \"go.mod\")); err == nil {\n\t\tpass(\"Go project detected (go.mod)\")\n\t}\n\tif _, err := os.Stat(filepath.Join(absWorkspace, \"package.json\")); err == nil {\n\t\tpass(\"Node project detected (package.json)\")\n\t}\n\n\twrite(\"Security:\")\n\tmode, _ := configpkg.NormalizeApprovalMode(cfg.ApprovalMode)\n\tpass(fmt.Sprintf(\"approval mode: %s\", mode))\n\tif cfg.SandboxEnabled {\n\t\tpass(fmt.Sprintf(\"sandbox: enabled (%s)\", cfg.SystemSandboxMode))\n\t} else {\n\t\twarn(\"sandbox: disabled\")\n\t}\n\tif len(cfg.WritableRoots) > 0 {\n\t\tpass(fmt.Sprintf(\"writable roots: %s\", strings.Join(cfg.WritableRoots, \", \")))\n\t} else {\n\t\twarn(\"no writable roots configured (defaults to workspace only)\")\n\t}\n\tif len(cfg.ExecAllowlist) > 0 {\n\t\tpass(fmt.Sprintf(\"exec allowlist: %d rule(s)\", len(cfg.ExecAllowlist)))\n\t} else {\n\t\twarn(\"no exec allowlist configured\")\n\t}\n\n\twrite(\"Environment:\")\n\tpass(fmt.Sprintf(\"OS: %s / Arch: %s\", runtime.GOOS, runtime.GOARCH))\n\tif _, err := exec.LookPath(\"go\"); err == nil {\n\t\tpass(\"Go is installed\")\n\t} else {\n\t\twarn(\"Go not found in PATH\")\n\t}\n\n\twrite(\"\")\n\twrite(\"Doctor check complete.\")\n\tif key == \"\" {\n\t\twrite(\"Set your API key via environment variable or config.json before using ByteMind.\")\n\t}\n\treturn nil\n}\n"}, "ac56e30a42ec1ebaec7ad0ea0ff136b9dfd8355f": {"t": 1785749486.509963, "n": 3462, "h": "8a05e3b025b45a1c", "c": "package app\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\tconfigpkg \"github.com/1024XEngineer/bytemind/internal/config\"\n)\n\nfunc RunDoctor(args []string, stdout, stderr io.Writer) error {\n\tworkspace := \".\"\n\tconfigFile := \"\"\n\tfor i := 0; i < len(args); i++ {\n\t\tswitch args[i] {\n\t\tcase \"-workspace\", \"--workspace\":\n\t\t\tif i+1 < len(args) {\n\t\t\t\tworkspace = args[i+1]\n\t\t\t\ti++\n\t\t\t}\n\t\tcase \"-config\", \"--config\":\n\t\t\tif i+1 < len(args) {\n\t\t\t\tconfigFile = args[i+1]\n\t\t\t\ti++\n\t\t\t}\n\t\t}\n\t}\n\n\twrite := func(format string, v ...any) {\n\t\tfmt.Fprintf(stdout, format+\"\\n\", v...)\n\t}\n\tpass := func(msg string) {\n\t\twrite(\" [PASS] %s\", msg)\n\t}\n\tfail := func(msg string) {\n\t\twrite(\" [FAIL] %s\", msg)\n\t}\n\twarn := func(msg string) {\n\t\twrite(\" [WARN] %s\", msg)\n\t}\n\n\twrite(\"ByteMind Doctor\")\n\twrite(\"\")\n\n\twrite(\"Configuration:\")\n\tcfg, err := configpkg.Load(workspace, configFile)\n\tconfigFound := err == nil\n\tif configFound {\n\t\tpass(\"config file loaded\")\n\t} else {\n\t\twarn(fmt.Sprintf(\"no config file found (optional): %v\", err))\n\t\tcfg = configpkg.Default(workspace)\n\t}\n\n\twrite(\"Provider:\")\n\tselected := configpkg.SelectedModelID(cfg.ProviderRuntime)\n\tif selected != \"\" {\n\t\tpass(fmt.Sprintf(\"model configured: %s\", selected))\n\t} else if cfg.Provider.Model != \"\" {\n\t\tpass(fmt.Sprintf(\"model configured: %s\", cfg.Provider.Model))\n\t} else {\n\t\twarn(\"no model configured\")\n\t}\n\tkey := cfg.Provider.APIKey\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"ANTHROPIC_API_KEY\")\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"OPENAI_API_KEY\")\n\t}\n\tif key == \"\" {\n\t\tkey = os.Getenv(\"DEEPSEEK_API_KEY\")\n\t}\n\tif key != \"\" {\n\t\tpass(\"API key detected\")\n\t} else {\n\t\tfail(\"no API key found (set ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPSEEK_API_KEY)\")\n\t}\n\n\twrite(\"Workspace:\")\n\tabsWorkspace, _ := filepath.Abs(workspace)\n\tif info, err := os.Stat(absWorkspace); err == nil && info.IsDir() {\n\t\tpass(fmt.Sprintf(\"workspace valid: %s\", absWorkspace))\n\t} else {\n\t\tfail(fmt.Sprintf(\"workspace not found: %s\", absWorkspace))\n\t}\n\tif _, err := exec.LookPath(\"git\"); err == nil {\n\t\tpass(\"git found in PATH\")\n\t} else {\n\t\twarn(\"git not found in PATH\")\n\t}\n\tif _, err := os.Stat(filepath.Join(absWorkspace, \".git\")); err == nil {\n\t\tpass(\"git repository detected\")\n\t}\n\tif _, err := os.Stat(filepath.Join(absWorkspace, \"go.mod\")); err == nil {\n\t\tpass(\"Go project detected (go.mod)\")\n\t}\n\tif _, err := os.Stat(filepath.Join(absWorkspace, \"package.json\")); err == nil {\n\t\tpass(\"Node project detected (package.json)\")\n\t}\n\n\twrite(\"Security:\")\n\tmode, _ := configpkg.NormalizeApprovalMode(cfg.ApprovalMode)\n\tpass(fmt.Sprintf(\"approval mode: %s\", mode))\n\tif cfg.SandboxEnabled {\n\t\tpass(fmt.Sprintf(\"sandbox: enabled (%s)\", cfg.SystemSandboxMode))\n\t} else {\n\t\twarn(\"sandbox: disabled\")\n\t}\n\tif len(cfg.WritableRoots) > 0 {\n\t\tpass(fmt.Sprintf(\"writable roots: %s\", strings.Join(cfg.WritableRoots, \", \")))\n\t} else {\n\t\twarn(\"no writable roots configured (defaults to workspace only)\")\n\t}\n\tif len(cfg.ExecAllowlist) > 0 {\n\t\tpass(fmt.Sprintf(\"exec allowlist: %d rule(s)\", len(cfg.ExecAllowlist)))\n\t} else {\n\t\twarn(\"no exec allowlist configured\")\n\t}\n\n\twrite(\"Environment:\")\n\tpass(fmt.Sprintf(\"OS: %s / Arch: %s\", runtime.GOOS, runtime.GOARCH))\n\tif _, err := exec.LookPath(\"go\"); err == nil {\n\t\tpass(\"Go is installed\")\n\t} else {\n\t\twarn(\"Go not found in PATH\")\n\t}\n\n\twrite(\"\")\n\twrite(\"Doctor check complete.\")\n\tif key == \"\" {\n\t\twrite(\"Set your API key via environment variable or config.json before using ByteMind.\")\n\t}\n\treturn nil\n}\n"}}, "internal/config/config.go": {"f259496ed7f959b3400c57e2d3d4d0a548d4b6a3": {"t": 1785745643.4093661, "n": 45660, "h": "a2b284850cda252f", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"github.com/1024XEngineer/bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultProviderID = \"deepseek\"\n\tdefaultModelID = \"deepseek-v4-flash\"\n\tlegacyOpenAIDefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 0\n\tDefaultMaxIterations = 64\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultContextBudgetFallback = 128000\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tFamily string `json:\"family\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tModels []string `json:\"models,omitempty\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: DefaultMaxIterations,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\tvar userProviderOverride map[string]json.RawMessage\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t\tuserProviderOverride, err = loadUserProviderOverride(userConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif err := mergeProviderOverride(userProviderOverride, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\toverrideProvider := applyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif overrideProvider {\n\t\tapplyProviderEnvOverrides(&cfg)\n\t\tcfg.ProviderRuntime = SyncProviderRuntimeWithProvider(cfg.ProviderRuntime, cfg.Provider)\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\tif value := strings.TrimSpace(os.Getenv(env)); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: DefaultMaxIterations,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\tif _, ok := raw[\"provider_runtime\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t} else if _, ok := raw[\"provider\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc loadUserProviderOverride(path string) (map[string]json.RawMessage, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasProviderCredential(raw) {\n\t\treturn nil, nil\n\t}\n\treturn raw, nil\n}\n\nfunc mergeProviderOverride(raw map[string]json.RawMessage, cfg *Config) error {\n\tif raw == nil || cfg == nil {\n\t\treturn nil\n\t}\n\tproviderOverridden := false\n\tif providerRaw, ok := raw[\"provider\"]; ok {\n\t\tif err := json.Unmarshal(providerRaw, &cfg.Provider); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproviderOverridden = true\n\t}\n\tif runtimeRaw, ok := raw[\"provider_runtime\"]; ok {\n\t\tif err := json.Unmarshal(runtimeRaw, &cfg.ProviderRuntime); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if providerOverridden {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\treturn nil\n}\n\nfunc hasProviderCredential(raw map[string]json.RawMessage) bool {\n\tif providerRaw, ok := raw[\"provider\"]; ok && providerRawHasCredential(providerRaw) {\n\t\treturn true\n\t}\n\truntimeRaw, ok := raw[\"provider_runtime\"]\n\tif !ok {\n\t\treturn false\n\t}\n\tvar runtime struct {\n\t\tProviders map[string]json.RawMessage `json:\"providers\"`\n\t}\n\tif err := json.Unmarshal(runtimeRaw, &runtime); err != nil {\n\t\treturn false\n\t}\n\tfor _, providerRaw := range runtime.Providers {\n\t\tif providerRawHasCredential(providerRaw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc providerRawHasCredential(raw json.RawMessage) bool {\n\tvar provider map[string]json.RawMessage\n\tif err := json.Unmarshal(raw, &provider); err != nil {\n\t\treturn false\n\t}\n\tif valueRaw, ok := provider[\"api_key\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(value) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\tif valueRaw, ok := provider[\"api_key_env\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(os.Getenv(strings.TrimSpace(value))) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) (providerOverride bool) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t\tproviderOverride = true\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n\treturn providerOverride\n}\n\nfunc applyProviderEnvOverrides(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\texplicitCurrentProvider := strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) != \"\"\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tcfg.Provider.Models = normalizeStringList(cfg.Provider.Models)\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) == \"\" && strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.CurrentProvider = legacy.CurrentProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.CurrentProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider))\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tif cfg.ProviderRuntime.CurrentProvider == \"\" {\n\t\tcfg.ProviderRuntime.CurrentProvider = cfg.ProviderRuntime.DefaultProvider\n\t}\n\tif cfg.ProviderRuntime.DefaultProvider == \"\" {\n\t\tcfg.ProviderRuntime.DefaultProvider = cfg.ProviderRuntime.CurrentProvider\n\t}\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tproviderCfg.Family = normalizeProviderFamily(providerCfg.Family)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tproviderCfg.Models = normalizeStringList(providerCfg.Models)\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tif explicitCurrentProvider {\n\t\tif _, ok := cfg.ProviderRuntime.Providers[cfg.ProviderRuntime.CurrentProvider]; !ok {\n\t\t\treturn fmt.Errorf(\"provider_runtime.current_provider %q is not configured\", cfg.ProviderRuntime.CurrentProvider)\n\t\t}\n\t}\n\tif providerID, providerCfg, ok := SelectedProviderConfig(cfg.ProviderRuntime); ok {\n\t\tcfg.ProviderRuntime.CurrentProvider = providerID\n\t\tcfg.ProviderRuntime.DefaultProvider = providerID\n\t\tif model := strings.TrimSpace(providerCfg.Model); model != \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = model\n\t\t}\n\t\tcfg.Provider = providerCfg\n\t}\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = DefaultMaxIterations\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 0 {\n\t\tcfg.TokenQuota = 0\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc normalizeProviderFamily(value string) string {\n\tvalue = strings.ToLower(strings.TrimSpace(value))\n\tvalue = strings.ReplaceAll(value, \"_\", \"-\")\n\tswitch value {\n\tcase \"moonshot-ai\":\n\t\treturn \"moonshot\"\n\tcase \"z-ai\", \"z.ai\", \"zhipu-ai\":\n\t\treturn \"zai\"\n\tdefault:\n\t\treturn value\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tbaseURL = strings.TrimRight(strings.TrimSpace(baseURL), \"/\")\n\t\treturn strings.EqualFold(baseURL, \"https://api.openai.com/v1\") ||\n\t\t\tstrings.EqualFold(baseURL, \"https://api.deepseek.com\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tmodel = strings.TrimSpace(model)\n\t\treturn model == defaultModelID || model == legacyOpenAIDefaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn defaultModelID\n\t\t}\n\t\treturn legacyOpenAIDefaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "32aea9886130f13bd9174c1f903405d53454c8a8": {"t": 1785749485.2559342, "n": 45660, "h": "a2b284850cda252f", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"github.com/1024XEngineer/bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultProviderID = \"deepseek\"\n\tdefaultModelID = \"deepseek-v4-flash\"\n\tlegacyOpenAIDefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 0\n\tDefaultMaxIterations = 64\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultContextBudgetFallback = 128000\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tFamily string `json:\"family\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tModels []string `json:\"models,omitempty\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: DefaultMaxIterations,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\tvar userProviderOverride map[string]json.RawMessage\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t\tuserProviderOverride, err = loadUserProviderOverride(userConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif err := mergeProviderOverride(userProviderOverride, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\toverrideProvider := applyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif overrideProvider {\n\t\tapplyProviderEnvOverrides(&cfg)\n\t\tcfg.ProviderRuntime = SyncProviderRuntimeWithProvider(cfg.ProviderRuntime, cfg.Provider)\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\tif value := strings.TrimSpace(os.Getenv(env)); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: DefaultMaxIterations,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\tif _, ok := raw[\"provider_runtime\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t} else if _, ok := raw[\"provider\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc loadUserProviderOverride(path string) (map[string]json.RawMessage, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasProviderCredential(raw) {\n\t\treturn nil, nil\n\t}\n\treturn raw, nil\n}\n\nfunc mergeProviderOverride(raw map[string]json.RawMessage, cfg *Config) error {\n\tif raw == nil || cfg == nil {\n\t\treturn nil\n\t}\n\tproviderOverridden := false\n\tif providerRaw, ok := raw[\"provider\"]; ok {\n\t\tif err := json.Unmarshal(providerRaw, &cfg.Provider); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproviderOverridden = true\n\t}\n\tif runtimeRaw, ok := raw[\"provider_runtime\"]; ok {\n\t\tif err := json.Unmarshal(runtimeRaw, &cfg.ProviderRuntime); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if providerOverridden {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\treturn nil\n}\n\nfunc hasProviderCredential(raw map[string]json.RawMessage) bool {\n\tif providerRaw, ok := raw[\"provider\"]; ok && providerRawHasCredential(providerRaw) {\n\t\treturn true\n\t}\n\truntimeRaw, ok := raw[\"provider_runtime\"]\n\tif !ok {\n\t\treturn false\n\t}\n\tvar runtime struct {\n\t\tProviders map[string]json.RawMessage `json:\"providers\"`\n\t}\n\tif err := json.Unmarshal(runtimeRaw, &runtime); err != nil {\n\t\treturn false\n\t}\n\tfor _, providerRaw := range runtime.Providers {\n\t\tif providerRawHasCredential(providerRaw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc providerRawHasCredential(raw json.RawMessage) bool {\n\tvar provider map[string]json.RawMessage\n\tif err := json.Unmarshal(raw, &provider); err != nil {\n\t\treturn false\n\t}\n\tif valueRaw, ok := provider[\"api_key\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(value) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\tif valueRaw, ok := provider[\"api_key_env\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(os.Getenv(strings.TrimSpace(value))) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) (providerOverride bool) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t\tproviderOverride = true\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n\treturn providerOverride\n}\n\nfunc applyProviderEnvOverrides(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\texplicitCurrentProvider := strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) != \"\"\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tcfg.Provider.Models = normalizeStringList(cfg.Provider.Models)\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) == \"\" && strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.CurrentProvider = legacy.CurrentProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.CurrentProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider))\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tif cfg.ProviderRuntime.CurrentProvider == \"\" {\n\t\tcfg.ProviderRuntime.CurrentProvider = cfg.ProviderRuntime.DefaultProvider\n\t}\n\tif cfg.ProviderRuntime.DefaultProvider == \"\" {\n\t\tcfg.ProviderRuntime.DefaultProvider = cfg.ProviderRuntime.CurrentProvider\n\t}\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tproviderCfg.Family = normalizeProviderFamily(providerCfg.Family)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tproviderCfg.Models = normalizeStringList(providerCfg.Models)\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tif explicitCurrentProvider {\n\t\tif _, ok := cfg.ProviderRuntime.Providers[cfg.ProviderRuntime.CurrentProvider]; !ok {\n\t\t\treturn fmt.Errorf(\"provider_runtime.current_provider %q is not configured\", cfg.ProviderRuntime.CurrentProvider)\n\t\t}\n\t}\n\tif providerID, providerCfg, ok := SelectedProviderConfig(cfg.ProviderRuntime); ok {\n\t\tcfg.ProviderRuntime.CurrentProvider = providerID\n\t\tcfg.ProviderRuntime.DefaultProvider = providerID\n\t\tif model := strings.TrimSpace(providerCfg.Model); model != \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = model\n\t\t}\n\t\tcfg.Provider = providerCfg\n\t}\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = DefaultMaxIterations\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 0 {\n\t\tcfg.TokenQuota = 0\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc normalizeProviderFamily(value string) string {\n\tvalue = strings.ToLower(strings.TrimSpace(value))\n\tvalue = strings.ReplaceAll(value, \"_\", \"-\")\n\tswitch value {\n\tcase \"moonshot-ai\":\n\t\treturn \"moonshot\"\n\tcase \"z-ai\", \"z.ai\", \"zhipu-ai\":\n\t\treturn \"zai\"\n\tdefault:\n\t\treturn value\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tbaseURL = strings.TrimRight(strings.TrimSpace(baseURL), \"/\")\n\t\treturn strings.EqualFold(baseURL, \"https://api.openai.com/v1\") ||\n\t\t\tstrings.EqualFold(baseURL, \"https://api.deepseek.com\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tmodel = strings.TrimSpace(model)\n\t\treturn model == defaultModelID || model == legacyOpenAIDefaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn defaultModelID\n\t\t}\n\t\treturn legacyOpenAIDefaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "57534d36db285111b5472c754bca4fe14d8f6bb1": {"t": 1785749486.5155642, "n": 45565, "h": "cf3a6d11d8f45a85", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"github.com/1024XEngineer/bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultProviderID = \"deepseek\"\n\tdefaultModelID = \"deepseek-v4-flash\"\n\tlegacyOpenAIDefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 0\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultContextBudgetFallback = 128000\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tFamily string `json:\"family\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tModels []string `json:\"models,omitempty\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\n\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\tvar userProviderOverride map[string]json.RawMessage\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t\tuserProviderOverride, err = loadUserProviderOverride(userConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif err := mergeProviderOverride(userProviderOverride, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\toverrideProvider := applyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif overrideProvider {\n\t\tapplyProviderEnvOverrides(&cfg)\n\t\tcfg.ProviderRuntime = SyncProviderRuntimeWithProvider(cfg.ProviderRuntime, cfg.Provider)\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\tif value := strings.TrimSpace(os.Getenv(env)); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\tif _, ok := raw[\"provider_runtime\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t} else if _, ok := raw[\"provider\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc loadUserProviderOverride(path string) (map[string]json.RawMessage, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasProviderCredential(raw) {\n\t\treturn nil, nil\n\t}\n\treturn raw, nil\n}\n\nfunc mergeProviderOverride(raw map[string]json.RawMessage, cfg *Config) error {\n\tif raw == nil || cfg == nil {\n\t\treturn nil\n\t}\n\tproviderOverridden := false\n\tif providerRaw, ok := raw[\"provider\"]; ok {\n\t\tif err := json.Unmarshal(providerRaw, &cfg.Provider); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproviderOverridden = true\n\t}\n\tif runtimeRaw, ok := raw[\"provider_runtime\"]; ok {\n\t\tif err := json.Unmarshal(runtimeRaw, &cfg.ProviderRuntime); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if providerOverridden {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\treturn nil\n}\n\nfunc hasProviderCredential(raw map[string]json.RawMessage) bool {\n\tif providerRaw, ok := raw[\"provider\"]; ok && providerRawHasCredential(providerRaw) {\n\t\treturn true\n\t}\n\truntimeRaw, ok := raw[\"provider_runtime\"]\n\tif !ok {\n\t\treturn false\n\t}\n\tvar runtime struct {\n\t\tProviders map[string]json.RawMessage `json:\"providers\"`\n\t}\n\tif err := json.Unmarshal(runtimeRaw, &runtime); err != nil {\n\t\treturn false\n\t}\n\tfor _, providerRaw := range runtime.Providers {\n\t\tif providerRawHasCredential(providerRaw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc providerRawHasCredential(raw json.RawMessage) bool {\n\tvar provider map[string]json.RawMessage\n\tif err := json.Unmarshal(raw, &provider); err != nil {\n\t\treturn false\n\t}\n\tif valueRaw, ok := provider[\"api_key\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(value) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\tif valueRaw, ok := provider[\"api_key_env\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(os.Getenv(strings.TrimSpace(value))) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) (providerOverride bool) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t\tproviderOverride = true\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n\treturn providerOverride\n}\n\nfunc applyProviderEnvOverrides(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\texplicitCurrentProvider := strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) != \"\"\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tcfg.Provider.Models = normalizeStringList(cfg.Provider.Models)\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) == \"\" && strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.CurrentProvider = legacy.CurrentProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.CurrentProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider))\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tif cfg.ProviderRuntime.CurrentProvider == \"\" {\n\t\tcfg.ProviderRuntime.CurrentProvider = cfg.ProviderRuntime.DefaultProvider\n\t}\n\tif cfg.ProviderRuntime.DefaultProvider == \"\" {\n\t\tcfg.ProviderRuntime.DefaultProvider = cfg.ProviderRuntime.CurrentProvider\n\t}\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tproviderCfg.Family = normalizeProviderFamily(providerCfg.Family)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tproviderCfg.Models = normalizeStringList(providerCfg.Models)\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tif explicitCurrentProvider {\n\t\tif _, ok := cfg.ProviderRuntime.Providers[cfg.ProviderRuntime.CurrentProvider]; !ok {\n\t\t\treturn fmt.Errorf(\"provider_runtime.current_provider %q is not configured\", cfg.ProviderRuntime.CurrentProvider)\n\t\t}\n\t}\n\tif providerID, providerCfg, ok := SelectedProviderConfig(cfg.ProviderRuntime); ok {\n\t\tcfg.ProviderRuntime.CurrentProvider = providerID\n\t\tcfg.ProviderRuntime.DefaultProvider = providerID\n\t\tif model := strings.TrimSpace(providerCfg.Model); model != \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = model\n\t\t}\n\t\tcfg.Provider = providerCfg\n\t}\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 0 {\n\t\tcfg.TokenQuota = 0\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc normalizeProviderFamily(value string) string {\n\tvalue = strings.ToLower(strings.TrimSpace(value))\n\tvalue = strings.ReplaceAll(value, \"_\", \"-\")\n\tswitch value {\n\tcase \"moonshot-ai\":\n\t\treturn \"moonshot\"\n\tcase \"z-ai\", \"z.ai\", \"zhipu-ai\":\n\t\treturn \"zai\"\n\tdefault:\n\t\treturn value\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tbaseURL = strings.TrimRight(strings.TrimSpace(baseURL), \"/\")\n\t\treturn strings.EqualFold(baseURL, \"https://api.openai.com/v1\") ||\n\t\t\tstrings.EqualFold(baseURL, \"https://api.deepseek.com\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tmodel = strings.TrimSpace(model)\n\t\treturn model == defaultModelID || model == legacyOpenAIDefaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn defaultModelID\n\t\t}\n\t\treturn legacyOpenAIDefaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "585dd83b26e075042edc4c646fc7743e3e02df0d": {"t": 1785749487.8254552, "n": 45518, "h": "5a383ea41cb6938f", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"github.com/1024XEngineer/bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultProviderID = \"deepseek\"\n\tdefaultModelID = \"deepseek-v4-flash\"\n\tlegacyOpenAIDefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 0\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tFamily string `json:\"family\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tModels []string `json:\"models,omitempty\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\n\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\tvar userProviderOverride map[string]json.RawMessage\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t\tuserProviderOverride, err = loadUserProviderOverride(userConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif err := mergeProviderOverride(userProviderOverride, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\toverrideProvider := applyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif overrideProvider {\n\t\tapplyProviderEnvOverrides(&cfg)\n\t\tcfg.ProviderRuntime = SyncProviderRuntimeWithProvider(cfg.ProviderRuntime, cfg.Provider)\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\tif value := strings.TrimSpace(os.Getenv(env)); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\tif _, ok := raw[\"provider_runtime\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t} else if _, ok := raw[\"provider\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc loadUserProviderOverride(path string) (map[string]json.RawMessage, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasProviderCredential(raw) {\n\t\treturn nil, nil\n\t}\n\treturn raw, nil\n}\n\nfunc mergeProviderOverride(raw map[string]json.RawMessage, cfg *Config) error {\n\tif raw == nil || cfg == nil {\n\t\treturn nil\n\t}\n\tproviderOverridden := false\n\tif providerRaw, ok := raw[\"provider\"]; ok {\n\t\tif err := json.Unmarshal(providerRaw, &cfg.Provider); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproviderOverridden = true\n\t}\n\tif runtimeRaw, ok := raw[\"provider_runtime\"]; ok {\n\t\tif err := json.Unmarshal(runtimeRaw, &cfg.ProviderRuntime); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if providerOverridden {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\treturn nil\n}\n\nfunc hasProviderCredential(raw map[string]json.RawMessage) bool {\n\tif providerRaw, ok := raw[\"provider\"]; ok && providerRawHasCredential(providerRaw) {\n\t\treturn true\n\t}\n\truntimeRaw, ok := raw[\"provider_runtime\"]\n\tif !ok {\n\t\treturn false\n\t}\n\tvar runtime struct {\n\t\tProviders map[string]json.RawMessage `json:\"providers\"`\n\t}\n\tif err := json.Unmarshal(runtimeRaw, &runtime); err != nil {\n\t\treturn false\n\t}\n\tfor _, providerRaw := range runtime.Providers {\n\t\tif providerRawHasCredential(providerRaw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc providerRawHasCredential(raw json.RawMessage) bool {\n\tvar provider map[string]json.RawMessage\n\tif err := json.Unmarshal(raw, &provider); err != nil {\n\t\treturn false\n\t}\n\tif valueRaw, ok := provider[\"api_key\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(value) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\tif valueRaw, ok := provider[\"api_key_env\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(os.Getenv(strings.TrimSpace(value))) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) (providerOverride bool) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t\tproviderOverride = true\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n\treturn providerOverride\n}\n\nfunc applyProviderEnvOverrides(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\texplicitCurrentProvider := strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) != \"\"\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tcfg.Provider.Models = normalizeStringList(cfg.Provider.Models)\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) == \"\" && strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.CurrentProvider = legacy.CurrentProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.CurrentProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider))\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tif cfg.ProviderRuntime.CurrentProvider == \"\" {\n\t\tcfg.ProviderRuntime.CurrentProvider = cfg.ProviderRuntime.DefaultProvider\n\t}\n\tif cfg.ProviderRuntime.DefaultProvider == \"\" {\n\t\tcfg.ProviderRuntime.DefaultProvider = cfg.ProviderRuntime.CurrentProvider\n\t}\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tproviderCfg.Family = normalizeProviderFamily(providerCfg.Family)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tproviderCfg.Models = normalizeStringList(providerCfg.Models)\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tif explicitCurrentProvider {\n\t\tif _, ok := cfg.ProviderRuntime.Providers[cfg.ProviderRuntime.CurrentProvider]; !ok {\n\t\t\treturn fmt.Errorf(\"provider_runtime.current_provider %q is not configured\", cfg.ProviderRuntime.CurrentProvider)\n\t\t}\n\t}\n\tif providerID, providerCfg, ok := SelectedProviderConfig(cfg.ProviderRuntime); ok {\n\t\tcfg.ProviderRuntime.CurrentProvider = providerID\n\t\tcfg.ProviderRuntime.DefaultProvider = providerID\n\t\tif model := strings.TrimSpace(providerCfg.Model); model != \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = model\n\t\t}\n\t\tcfg.Provider = providerCfg\n\t}\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 0 {\n\t\tcfg.TokenQuota = 0\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc normalizeProviderFamily(value string) string {\n\tvalue = strings.ToLower(strings.TrimSpace(value))\n\tvalue = strings.ReplaceAll(value, \"_\", \"-\")\n\tswitch value {\n\tcase \"moonshot-ai\":\n\t\treturn \"moonshot\"\n\tcase \"z-ai\", \"z.ai\", \"zhipu-ai\":\n\t\treturn \"zai\"\n\tdefault:\n\t\treturn value\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tbaseURL = strings.TrimRight(strings.TrimSpace(baseURL), \"/\")\n\t\treturn strings.EqualFold(baseURL, \"https://api.openai.com/v1\") ||\n\t\t\tstrings.EqualFold(baseURL, \"https://api.deepseek.com\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tmodel = strings.TrimSpace(model)\n\t\treturn model == defaultModelID || model == legacyOpenAIDefaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn defaultModelID\n\t\t}\n\t\treturn legacyOpenAIDefaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "f8c89c1b611784c7a3a16757e87f1c3c16cea15c": {"t": 1785749489.1462657, "n": 45539, "h": "a2e5bd1425c769ec", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"github.com/1024XEngineer/bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultProviderID = \"deepseek\"\n\tdefaultModelID = \"deepseek-v4-flash\"\n\tlegacyOpenAIDefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tFamily string `json:\"family\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tModels []string `json:\"models,omitempty\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\n\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\tvar userProviderOverride map[string]json.RawMessage\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t\tuserProviderOverride, err = loadUserProviderOverride(userConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif err := mergeProviderOverride(userProviderOverride, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\toverrideProvider := applyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif overrideProvider {\n\t\tapplyProviderEnvOverrides(&cfg)\n\t\tcfg.ProviderRuntime = SyncProviderRuntimeWithProvider(cfg.ProviderRuntime, cfg.Provider)\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\tif value := strings.TrimSpace(os.Getenv(env)); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\tif _, ok := raw[\"provider_runtime\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t} else if _, ok := raw[\"provider\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc loadUserProviderOverride(path string) (map[string]json.RawMessage, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasProviderCredential(raw) {\n\t\treturn nil, nil\n\t}\n\treturn raw, nil\n}\n\nfunc mergeProviderOverride(raw map[string]json.RawMessage, cfg *Config) error {\n\tif raw == nil || cfg == nil {\n\t\treturn nil\n\t}\n\tproviderOverridden := false\n\tif providerRaw, ok := raw[\"provider\"]; ok {\n\t\tif err := json.Unmarshal(providerRaw, &cfg.Provider); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproviderOverridden = true\n\t}\n\tif runtimeRaw, ok := raw[\"provider_runtime\"]; ok {\n\t\tif err := json.Unmarshal(runtimeRaw, &cfg.ProviderRuntime); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if providerOverridden {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\treturn nil\n}\n\nfunc hasProviderCredential(raw map[string]json.RawMessage) bool {\n\tif providerRaw, ok := raw[\"provider\"]; ok && providerRawHasCredential(providerRaw) {\n\t\treturn true\n\t}\n\truntimeRaw, ok := raw[\"provider_runtime\"]\n\tif !ok {\n\t\treturn false\n\t}\n\tvar runtime struct {\n\t\tProviders map[string]json.RawMessage `json:\"providers\"`\n\t}\n\tif err := json.Unmarshal(runtimeRaw, &runtime); err != nil {\n\t\treturn false\n\t}\n\tfor _, providerRaw := range runtime.Providers {\n\t\tif providerRawHasCredential(providerRaw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc providerRawHasCredential(raw json.RawMessage) bool {\n\tvar provider map[string]json.RawMessage\n\tif err := json.Unmarshal(raw, &provider); err != nil {\n\t\treturn false\n\t}\n\tif valueRaw, ok := provider[\"api_key\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(value) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\tif valueRaw, ok := provider[\"api_key_env\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(os.Getenv(strings.TrimSpace(value))) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) (providerOverride bool) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t\tproviderOverride = true\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n\treturn providerOverride\n}\n\nfunc applyProviderEnvOverrides(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\texplicitCurrentProvider := strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) != \"\"\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tcfg.Provider.Models = normalizeStringList(cfg.Provider.Models)\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) == \"\" && strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.CurrentProvider = legacy.CurrentProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.CurrentProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider))\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tif cfg.ProviderRuntime.CurrentProvider == \"\" {\n\t\tcfg.ProviderRuntime.CurrentProvider = cfg.ProviderRuntime.DefaultProvider\n\t}\n\tif cfg.ProviderRuntime.DefaultProvider == \"\" {\n\t\tcfg.ProviderRuntime.DefaultProvider = cfg.ProviderRuntime.CurrentProvider\n\t}\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tproviderCfg.Family = normalizeProviderFamily(providerCfg.Family)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tproviderCfg.Models = normalizeStringList(providerCfg.Models)\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tif explicitCurrentProvider {\n\t\tif _, ok := cfg.ProviderRuntime.Providers[cfg.ProviderRuntime.CurrentProvider]; !ok {\n\t\t\treturn fmt.Errorf(\"provider_runtime.current_provider %q is not configured\", cfg.ProviderRuntime.CurrentProvider)\n\t\t}\n\t}\n\tif providerID, providerCfg, ok := SelectedProviderConfig(cfg.ProviderRuntime); ok {\n\t\tcfg.ProviderRuntime.CurrentProvider = providerID\n\t\tcfg.ProviderRuntime.DefaultProvider = providerID\n\t\tif model := strings.TrimSpace(providerCfg.Model); model != \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = model\n\t\t}\n\t\tcfg.Provider = providerCfg\n\t}\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc normalizeProviderFamily(value string) string {\n\tvalue = strings.ToLower(strings.TrimSpace(value))\n\tvalue = strings.ReplaceAll(value, \"_\", \"-\")\n\tswitch value {\n\tcase \"moonshot-ai\":\n\t\treturn \"moonshot\"\n\tcase \"z-ai\", \"z.ai\", \"zhipu-ai\":\n\t\treturn \"zai\"\n\tdefault:\n\t\treturn value\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tbaseURL = strings.TrimRight(strings.TrimSpace(baseURL), \"/\")\n\t\treturn strings.EqualFold(baseURL, \"https://api.openai.com/v1\") ||\n\t\t\tstrings.EqualFold(baseURL, \"https://api.deepseek.com\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tmodel = strings.TrimSpace(model)\n\t\treturn model == defaultModelID || model == legacyOpenAIDefaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn defaultModelID\n\t\t}\n\t\treturn legacyOpenAIDefaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "70025577667b3b1f246ce915125cfacb5da2f034": {"t": 1785749490.4746644, "n": 47452, "h": "bd7ac28315765d9d", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"github.com/1024XEngineer/bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultProviderID = \"deepseek\"\n\tdefaultModelID = \"deepseek-v4-flash\"\n\tlegacyOpenAIDefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tFamily string `json:\"family\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tModels []string `json:\"models,omitempty\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\tvar userProviderOverride map[string]json.RawMessage\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t\tuserProviderOverride, err = loadUserProviderOverride(userConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif err := mergeProviderOverride(userProviderOverride, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\toverrideProvider := applyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif overrideProvider {\n\t\tapplyProviderEnvOverrides(&cfg)\n\t\tcfg.ProviderRuntime = SyncProviderRuntimeWithProvider(cfg.ProviderRuntime, cfg.Provider)\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\tif value := strings.TrimSpace(os.Getenv(env)); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\tif _, ok := raw[\"provider_runtime\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t} else if _, ok := raw[\"provider\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc loadUserProviderOverride(path string) (map[string]json.RawMessage, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasProviderCredential(raw) {\n\t\treturn nil, nil\n\t}\n\treturn raw, nil\n}\n\nfunc mergeProviderOverride(raw map[string]json.RawMessage, cfg *Config) error {\n\tif raw == nil || cfg == nil {\n\t\treturn nil\n\t}\n\tproviderOverridden := false\n\tif providerRaw, ok := raw[\"provider\"]; ok {\n\t\tif err := json.Unmarshal(providerRaw, &cfg.Provider); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproviderOverridden = true\n\t}\n\tif runtimeRaw, ok := raw[\"provider_runtime\"]; ok {\n\t\tif err := json.Unmarshal(runtimeRaw, &cfg.ProviderRuntime); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if providerOverridden {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\treturn nil\n}\n\nfunc hasProviderCredential(raw map[string]json.RawMessage) bool {\n\tif providerRaw, ok := raw[\"provider\"]; ok && providerRawHasCredential(providerRaw) {\n\t\treturn true\n\t}\n\truntimeRaw, ok := raw[\"provider_runtime\"]\n\tif !ok {\n\t\treturn false\n\t}\n\tvar runtime struct {\n\t\tProviders map[string]json.RawMessage `json:\"providers\"`\n\t}\n\tif err := json.Unmarshal(runtimeRaw, &runtime); err != nil {\n\t\treturn false\n\t}\n\tfor _, providerRaw := range runtime.Providers {\n\t\tif providerRawHasCredential(providerRaw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc providerRawHasCredential(raw json.RawMessage) bool {\n\tvar provider map[string]json.RawMessage\n\tif err := json.Unmarshal(raw, &provider); err != nil {\n\t\treturn false\n\t}\n\tif valueRaw, ok := provider[\"api_key\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(value) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\tif valueRaw, ok := provider[\"api_key_env\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(os.Getenv(strings.TrimSpace(value))) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) (providerOverride bool) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t\tproviderOverride = true\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n\treturn providerOverride\n}\n\nfunc applyProviderEnvOverrides(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\texplicitCurrentProvider := strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) != \"\"\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tcfg.Provider.Models = normalizeStringList(cfg.Provider.Models)\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) == \"\" && strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.CurrentProvider = legacy.CurrentProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.CurrentProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider))\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tif cfg.ProviderRuntime.CurrentProvider == \"\" {\n\t\tcfg.ProviderRuntime.CurrentProvider = cfg.ProviderRuntime.DefaultProvider\n\t}\n\tif cfg.ProviderRuntime.DefaultProvider == \"\" {\n\t\tcfg.ProviderRuntime.DefaultProvider = cfg.ProviderRuntime.CurrentProvider\n\t}\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tproviderCfg.Family = normalizeProviderFamily(providerCfg.Family)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tproviderCfg.Models = normalizeStringList(providerCfg.Models)\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tif explicitCurrentProvider {\n\t\tif _, ok := cfg.ProviderRuntime.Providers[cfg.ProviderRuntime.CurrentProvider]; !ok {\n\t\t\treturn fmt.Errorf(\"provider_runtime.current_provider %q is not configured\", cfg.ProviderRuntime.CurrentProvider)\n\t\t}\n\t}\n\tif providerID, providerCfg, ok := SelectedProviderConfig(cfg.ProviderRuntime); ok {\n\t\tcfg.ProviderRuntime.CurrentProvider = providerID\n\t\tcfg.ProviderRuntime.DefaultProvider = providerID\n\t\tif model := strings.TrimSpace(providerCfg.Model); model != \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = model\n\t\t}\n\t\tcfg.Provider = providerCfg\n\t}\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc normalizeProviderFamily(value string) string {\n\tvalue = strings.ToLower(strings.TrimSpace(value))\n\tvalue = strings.ReplaceAll(value, \"_\", \"-\")\n\tswitch value {\n\tcase \"moonshot-ai\":\n\t\treturn \"moonshot\"\n\tcase \"z-ai\", \"z.ai\", \"zhipu-ai\":\n\t\treturn \"zai\"\n\tdefault:\n\t\treturn value\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tbaseURL = strings.TrimRight(strings.TrimSpace(baseURL), \"/\")\n\t\treturn strings.EqualFold(baseURL, \"https://api.openai.com/v1\") ||\n\t\t\tstrings.EqualFold(baseURL, \"https://api.deepseek.com\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tmodel = strings.TrimSpace(model)\n\t\treturn model == defaultModelID || model == legacyOpenAIDefaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn defaultModelID\n\t\t}\n\t\treturn legacyOpenAIDefaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "917d34a0c8a8b72282af1a2fdd22c13ad1efe909": {"t": 1785749491.932873, "n": 46318, "h": "8090672c8c0e55b8", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"github.com/1024XEngineer/bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultProviderID = \"deepseek\"\n\tdefaultModelID = \"deepseek-v4-flash\"\n\tlegacyOpenAIDefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tFamily string `json:\"family\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tModels []string `json:\"models,omitempty\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\tvar userProviderOverride map[string]json.RawMessage\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t\tuserProviderOverride, err = loadUserProviderOverride(userConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif err := mergeProviderOverride(userProviderOverride, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\tif value := strings.TrimSpace(os.Getenv(env)); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\tModel: defaultModelID,\n\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tProviderRuntime: ProviderRuntimeConfig{\n\t\t\tCurrentProvider: defaultProviderID,\n\t\t\tDefaultProvider: defaultProviderID,\n\t\t\tDefaultModel: defaultModelID,\n\t\t\tProviders: map[string]ProviderConfig{\n\t\t\t\tdefaultProviderID: {\n\t\t\t\t\tType: \"openai-compatible\",\n\t\t\t\t\tBaseURL: \"https://api.deepseek.com\",\n\t\t\t\t\tModel: defaultModelID,\n\t\t\t\t\tModels: []string{defaultModelID, \"deepseek-v4-pro\"},\n\t\t\t\t\tAPIKeyEnv: \"DEEPSEEK_API_KEY\",\n\t\t\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\tif _, ok := raw[\"provider_runtime\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t} else if _, ok := raw[\"provider\"]; ok {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc loadUserProviderOverride(path string) (map[string]json.RawMessage, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasProviderCredential(raw) {\n\t\treturn nil, nil\n\t}\n\treturn raw, nil\n}\n\nfunc mergeProviderOverride(raw map[string]json.RawMessage, cfg *Config) error {\n\tif raw == nil || cfg == nil {\n\t\treturn nil\n\t}\n\tproviderOverridden := false\n\tif providerRaw, ok := raw[\"provider\"]; ok {\n\t\tif err := json.Unmarshal(providerRaw, &cfg.Provider); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproviderOverridden = true\n\t}\n\tif runtimeRaw, ok := raw[\"provider_runtime\"]; ok {\n\t\tif err := json.Unmarshal(runtimeRaw, &cfg.ProviderRuntime); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if providerOverridden {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\treturn nil\n}\n\nfunc hasProviderCredential(raw map[string]json.RawMessage) bool {\n\tif providerRaw, ok := raw[\"provider\"]; ok && providerRawHasCredential(providerRaw) {\n\t\treturn true\n\t}\n\truntimeRaw, ok := raw[\"provider_runtime\"]\n\tif !ok {\n\t\treturn false\n\t}\n\tvar runtime struct {\n\t\tProviders map[string]json.RawMessage `json:\"providers\"`\n\t}\n\tif err := json.Unmarshal(runtimeRaw, &runtime); err != nil {\n\t\treturn false\n\t}\n\tfor _, providerRaw := range runtime.Providers {\n\t\tif providerRawHasCredential(providerRaw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc providerRawHasCredential(raw json.RawMessage) bool {\n\tvar provider map[string]json.RawMessage\n\tif err := json.Unmarshal(raw, &provider); err != nil {\n\t\treturn false\n\t}\n\tif valueRaw, ok := provider[\"api_key\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(value) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\tif valueRaw, ok := provider[\"api_key_env\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(os.Getenv(strings.TrimSpace(value))) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tproviderOverride := false\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t\tproviderOverride = true\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t\tproviderOverride = true\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n\tif providerOverride {\n\t\tcfg.ProviderRuntime = SyncProviderRuntimeWithProvider(cfg.ProviderRuntime, cfg.Provider)\n\t}\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\texplicitCurrentProvider := strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) != \"\"\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tcfg.Provider.Models = normalizeStringList(cfg.Provider.Models)\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider) == \"\" && strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.CurrentProvider = legacy.CurrentProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.CurrentProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.CurrentProvider))\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tif cfg.ProviderRuntime.CurrentProvider == \"\" {\n\t\tcfg.ProviderRuntime.CurrentProvider = cfg.ProviderRuntime.DefaultProvider\n\t}\n\tif cfg.ProviderRuntime.DefaultProvider == \"\" {\n\t\tcfg.ProviderRuntime.DefaultProvider = cfg.ProviderRuntime.CurrentProvider\n\t}\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tproviderCfg.Family = normalizeProviderFamily(providerCfg.Family)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tproviderCfg.Models = normalizeStringList(providerCfg.Models)\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tif explicitCurrentProvider {\n\t\tif _, ok := cfg.ProviderRuntime.Providers[cfg.ProviderRuntime.CurrentProvider]; !ok {\n\t\t\treturn fmt.Errorf(\"provider_runtime.current_provider %q is not configured\", cfg.ProviderRuntime.CurrentProvider)\n\t\t}\n\t}\n\tif providerID, providerCfg, ok := SelectedProviderConfig(cfg.ProviderRuntime); ok {\n\t\tcfg.ProviderRuntime.CurrentProvider = providerID\n\t\tcfg.ProviderRuntime.DefaultProvider = providerID\n\t\tif model := strings.TrimSpace(providerCfg.Model); model != \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = model\n\t\t}\n\t\tcfg.Provider = providerCfg\n\t}\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc normalizeProviderFamily(value string) string {\n\tvalue = strings.ToLower(strings.TrimSpace(value))\n\tvalue = strings.ReplaceAll(value, \"_\", \"-\")\n\tswitch value {\n\tcase \"moonshot-ai\":\n\t\treturn \"moonshot\"\n\tcase \"z-ai\", \"z.ai\", \"zhipu-ai\":\n\t\treturn \"zai\"\n\tdefault:\n\t\treturn value\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tbaseURL = strings.TrimRight(strings.TrimSpace(baseURL), \"/\")\n\t\treturn strings.EqualFold(baseURL, \"https://api.openai.com/v1\") ||\n\t\t\tstrings.EqualFold(baseURL, \"https://api.deepseek.com\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\tmodel = strings.TrimSpace(model)\n\t\treturn model == defaultModelID || model == legacyOpenAIDefaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn defaultModelID\n\t\t}\n\t\treturn legacyOpenAIDefaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "25c1d52c8e320efe7d3a597cdbed13d888c55f49": {"t": 1785749493.2499712, "n": 42880, "h": "50b5ae12d733cb23", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"github.com/1024XEngineer/bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tFamily string `json:\"family\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\tvar userProviderOverride map[string]json.RawMessage\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t\tuserProviderOverride, err = loadUserProviderOverride(userConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif err := mergeProviderOverride(userProviderOverride, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc loadUserProviderOverride(path string) (map[string]json.RawMessage, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn nil, err\n\t}\n\tif !hasProviderCredential(raw) {\n\t\treturn nil, nil\n\t}\n\treturn raw, nil\n}\n\nfunc mergeProviderOverride(raw map[string]json.RawMessage, cfg *Config) error {\n\tif raw == nil || cfg == nil {\n\t\treturn nil\n\t}\n\tproviderOverridden := false\n\tif providerRaw, ok := raw[\"provider\"]; ok {\n\t\tif err := json.Unmarshal(providerRaw, &cfg.Provider); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tproviderOverridden = true\n\t}\n\tif runtimeRaw, ok := raw[\"provider_runtime\"]; ok {\n\t\tif err := json.Unmarshal(runtimeRaw, &cfg.ProviderRuntime); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if providerOverridden {\n\t\tcfg.ProviderRuntime = ProviderRuntimeConfig{}\n\t}\n\treturn nil\n}\n\nfunc hasProviderCredential(raw map[string]json.RawMessage) bool {\n\tif providerRaw, ok := raw[\"provider\"]; ok && providerRawHasCredential(providerRaw) {\n\t\treturn true\n\t}\n\truntimeRaw, ok := raw[\"provider_runtime\"]\n\tif !ok {\n\t\treturn false\n\t}\n\tvar runtime struct {\n\t\tProviders map[string]json.RawMessage `json:\"providers\"`\n\t}\n\tif err := json.Unmarshal(runtimeRaw, &runtime); err != nil {\n\t\treturn false\n\t}\n\tfor _, providerRaw := range runtime.Providers {\n\t\tif providerRawHasCredential(providerRaw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc providerRawHasCredential(raw json.RawMessage) bool {\n\tvar provider map[string]json.RawMessage\n\tif err := json.Unmarshal(raw, &provider); err != nil {\n\t\treturn false\n\t}\n\tif valueRaw, ok := provider[\"api_key\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(value) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\tif valueRaw, ok := provider[\"api_key_env\"]; ok {\n\t\tvar value string\n\t\tif err := json.Unmarshal(valueRaw, &value); err == nil && strings.TrimSpace(os.Getenv(strings.TrimSpace(value))) != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tproviderCfg.Family = normalizeProviderFamily(providerCfg.Family)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc normalizeProviderFamily(value string) string {\n\tvalue = strings.ToLower(strings.TrimSpace(value))\n\tvalue = strings.ReplaceAll(value, \"_\", \"-\")\n\tswitch value {\n\tcase \"moonshot-ai\":\n\t\treturn \"moonshot\"\n\tcase \"z-ai\", \"z.ai\", \"zhipu-ai\":\n\t\treturn \"zai\"\n\tdefault:\n\t\treturn value\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.EqualFold(strings.TrimRight(strings.TrimSpace(baseURL), \"/\"), \"https://api.openai.com/v1\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.TrimSpace(model) == defaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "d16e4291ca2620cb47d2ae0b238913cfb532065b": {"t": 1785749494.6889188, "n": 40598, "h": "2e2a2444d7a2ef99", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"github.com/1024XEngineer/bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tFamily string `json:\"family\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_FAMILY\")); value != \"\" {\n\t\tcfg.Provider.Family = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tcfg.Provider.Family = normalizeProviderFamily(cfg.Provider.Family)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tproviderCfg.Family = normalizeProviderFamily(providerCfg.Family)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc normalizeProviderFamily(value string) string {\n\tvalue = strings.ToLower(strings.TrimSpace(value))\n\tvalue = strings.ReplaceAll(value, \"_\", \"-\")\n\tswitch value {\n\tcase \"moonshot-ai\":\n\t\treturn \"moonshot\"\n\tcase \"z-ai\", \"z.ai\", \"zhipu-ai\":\n\t\treturn \"zai\"\n\tdefault:\n\t\treturn value\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.EqualFold(strings.TrimRight(strings.TrimSpace(baseURL), \"/\"), \"https://api.openai.com/v1\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.TrimSpace(model) == defaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "3065ddf03bc22fcacac6f46755fd1c3d8df8b247": {"t": 1785749496.558314, "n": 39983, "h": "49c8b498ec1587b8", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tNotifications NotificationsConfig `json:\"notifications\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype NotificationsConfig struct {\n\tDesktop DesktopNotificationConfig `json:\"desktop\"`\n}\n\ntype DesktopNotificationConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tOnApprovalRequired bool `json:\"on_approval_required\"`\n\tOnRunCompleted bool `json:\"on_run_completed\"`\n\tOnRunFailed bool `json:\"on_run_failed\"`\n\tOnRunCanceled bool `json:\"on_run_canceled\"`\n\tCooldownSeconds int `json:\"cooldown_seconds\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tNotifications: NotificationsConfig{\n\t\t\tDesktop: DesktopNotificationConfig{\n\t\t\t\tEnabled: true,\n\t\t\t\tOnApprovalRequired: true,\n\t\t\t\tOnRunCompleted: true,\n\t\t\t\tOnRunFailed: true,\n\t\t\t\tOnRunCanceled: false,\n\t\t\t\tCooldownSeconds: 3,\n\t\t\t},\n\t\t},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_DESKTOP_NOTIFY\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_APPROVAL\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnApprovalRequired = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_COMPLETED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCompleted = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_FAILED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunFailed = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_RUN_CANCELED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.OnRunCanceled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_NOTIFY_COOLDOWN_SECONDS\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.Notifications.Desktop.CooldownSeconds = parsed\n\t\t}\n\t}\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif cfg.Notifications.Desktop.CooldownSeconds < 0 {\n\t\treturn errors.New(\"notifications.desktop.cooldown_seconds must be >= 0\")\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.EqualFold(strings.TrimRight(strings.TrimSpace(baseURL), \"/\"), \"https://api.openai.com/v1\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.TrimSpace(model) == defaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "9f1f401e54994cb5b990288498edda4635cba3b3": {"t": 1785749497.8120759, "n": 37613, "h": "b2a50d81270914a9", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n\t// EnvAllowAwayFullAccessCompat is a temporary migration gate that permits\n\t// approval_mode=away to map to full_access.\n\tEnvAllowAwayFullAccessCompat = \"BYTEMIND_ALLOW_AWAY_FULL_ACCESS\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\n// NormalizeApprovalMode validates and normalizes approval_mode values.\n// By default, legacy away mode is blocked to prevent silent privilege\n// escalation to full_access. Operators can temporarily enable migration by\n// setting BYTEMIND_ALLOW_AWAY_FULL_ACCESS=true.\nfunc NormalizeApprovalMode(value string) (string, error) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\", \"interactive\":\n\t\treturn \"interactive\", nil\n\tcase \"full_access\":\n\t\treturn \"full_access\", nil\n\tcase \"away\":\n\t\tif allowAwayFullAccessCompat() {\n\t\t\treturn \"full_access\", nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"approval_mode=away is blocked by default to prevent silent privilege escalation; migrate to approval_mode=interactive or approval_mode=full_access, or set %s=true for temporary migration\", EnvAllowAwayFullAccessCompat)\n\tdefault:\n\t\treturn \"\", errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n}\n\nfunc allowAwayFullAccessCompat() bool {\n\traw := strings.TrimSpace(os.Getenv(EnvAllowAwayFullAccessCompat))\n\tif raw == \"\" {\n\t\treturn false\n\t}\n\tenabled, err := strconv.ParseBool(raw)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn enabled\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tnormalizedApprovalMode, err := NormalizeApprovalMode(cfg.ApprovalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.ApprovalMode = normalizedApprovalMode\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.EqualFold(strings.TrimRight(strings.TrimSpace(baseURL), \"/\"), \"https://api.openai.com/v1\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.TrimSpace(model) == defaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "af26b6a324a1a08696faf590f521a95f08719c25": {"t": 1785749499.9938693, "n": 36439, "h": "ab0656e8803c863e", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"full_access\":\n\t\tcfg.ApprovalMode = \"full_access\"\n\tcase \"away\":\n\t\tcfg.ApprovalMode = \"full_access\"\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, full_access\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.EqualFold(strings.TrimRight(strings.TrimSpace(baseURL), \"/\"), \"https://api.openai.com/v1\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.TrimSpace(model) == defaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "8ba56a9e0a7e6907de82782e4f51261e2c3f7d02": {"t": 1785749501.396728, "n": 36341, "h": "ec493d04ce55c685", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" || usesOpenAIDefaultBaseURLForNativeProvider(cfg.Provider.Type, cfg.Provider.BaseURL) {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" || usesOpenAIDefaultModelForNativeProvider(cfg.Provider.Type, cfg.Provider.Model) {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic, gemini (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tcase \"gemini\", \"google\", \"google-gemini\":\n\t\treturn \"gemini\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-goog-api-key\" || hasHeaderName(cfg.ExtraHeaders, \"x-goog-api-key\") {\n\t\treturn \"gemini\"\n\t}\n\tif strings.Contains(baseURL, \"generativelanguage.googleapis.com\") && !strings.Contains(baseURL, \"/openai\") {\n\t\treturn \"gemini\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\", \"gemini\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tcase \"gemini\":\n\t\treturn \"https://generativelanguage.googleapis.com/v1beta\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc usesOpenAIDefaultBaseURLForNativeProvider(providerType, baseURL string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.EqualFold(strings.TrimRight(strings.TrimSpace(baseURL), \"/\"), \"https://api.openai.com/v1\")\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc usesOpenAIDefaultModelForNativeProvider(providerType, model string) bool {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\", \"gemini\":\n\t\treturn strings.TrimSpace(model) == defaultModelID\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tcase \"gemini\":\n\t\treturn \"gemini-2.5-flash\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "bfff580e98f486eeb8d58d9bc13fd6e4279a45e5": {"t": 1785749502.957437, "n": 35196, "h": "3424e8d03ffb963b", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"-\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\treturn LoadWithMCPConfigPath(workspace, configPath, \"\")\n}\n\nfunc LoadWithMCPConfigPath(workspace, configPath, mcpConfigPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tif strings.TrimSpace(mcpConfigPath) != \"\" {\n\t\tpath, err := filepath.Abs(mcpConfigPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeMCPConfigFromFile(path, &cfg.MCP); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif _, ok := root[\"mcp\"]; ok {\n\t\t\treturn errors.New(\"mcp config files must use top-level MCP fields; move mcp.* to the root\")\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "2cb9478bce493e0c137e60bb114f95128326fcdb": {"t": 1785749504.3126972, "n": 34904, "h": "ef17f2e444cb9d91", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"mcp\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif userMCPConfig, err := resolveUserMCPConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(userMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveUserMCPConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"mcp.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tpayload := data\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif nested, ok := root[\"mcp\"]; ok && len(nested) > 0 {\n\t\t\tpayload = nested\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(payload, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "65acaacc2d42773d501081fbfc918e9d774663a7": {"t": 1785749506.836528, "n": 34422, "h": "40b9aa4854e876d4", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"mcp\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t\tif projectMCPConfig := resolveProjectMCPConfigPath(workspace); projectMCPConfig != \"\" {\n\t\t\tif err := mergeMCPConfigFromFile(projectMCPConfig, &cfg.MCP); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveProjectMCPConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"mcp.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc mergeMCPConfigFromFile(path string, cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(string(data)) == \"\" {\n\t\treturn nil\n\t}\n\n\tpayload := data\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &root); err == nil {\n\t\tif nested, ok := root[\"mcp\"]; ok && len(nested) > 0 {\n\t\t\tpayload = nested\n\t\t}\n\t}\n\n\tif err := json.Unmarshal(payload, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "47936a746149554a088137082f42157b2512d909": {"t": 1785749508.2532184, "n": 33455, "h": "e8949146f4e2af61", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"mcp\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "3dc1147480094be5174168b7347d09bc8671f69a": {"t": 1785749509.667749, "n": 37007, "h": "577c81903d0adde4", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n\tDefaultExtensionsHealthCheckInterval = 30\n\tDefaultExtensionsFailureThreshold = 3\n\tDefaultExtensionsRecoveryCooldownSec = 30\n\tDefaultExtensionsMaxConcurrency = 4\n\tDefaultExtensionsConflictPolicy = \"reject\"\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tSystemSandboxMode string `json:\"system_sandbox_mode\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tExtensions ExtensionsConfig `json:\"extensions\"`\n\tMCP MCPConfig `json:\"mcp\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExtensionsConfig struct {\n\tSources []string `json:\"sources\"`\n\tAutoLoad *bool `json:\"auto_load,omitempty\"`\n\tHealthCheckIntervalSec int `json:\"health_check_interval_sec\"`\n\tFailureThreshold int `json:\"failure_threshold\"`\n\tRecoveryCooldownSec int `json:\"recovery_cooldown_sec\"`\n\tMaxConcurrencyPerExtension int `json:\"max_concurrency_per_extension\"`\n\tConflictPolicy string `json:\"conflict_policy\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc (cfg ExtensionsConfig) AutoLoadValue() bool {\n\tif cfg.AutoLoad == nil {\n\t\treturn true\n\t}\n\treturn *cfg.AutoLoad\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tExtensions: ExtensionsConfig{\n\t\t\tSources: []string{\"skills\", \"mcp\"},\n\t\t\tAutoLoad: boolPtr(true),\n\t\t\tHealthCheckIntervalSec: DefaultExtensionsHealthCheckInterval,\n\t\t\tFailureThreshold: DefaultExtensionsFailureThreshold,\n\t\t\tRecoveryCooldownSec: DefaultExtensionsRecoveryCooldownSec,\n\t\t\tMaxConcurrencyPerExtension: DefaultExtensionsMaxConcurrency,\n\t\t\tConflictPolicy: DefaultExtensionsConflictPolicy,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tSystemSandboxMode: \"off\",\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tExtensions: ExtensionsConfig{\n\t\t\tSources: []string{\"skills\", \"mcp\"},\n\t\t\tAutoLoad: boolPtr(true),\n\t\t\tHealthCheckIntervalSec: DefaultExtensionsHealthCheckInterval,\n\t\t\tFailureThreshold: DefaultExtensionsFailureThreshold,\n\t\t\tRecoveryCooldownSec: DefaultExtensionsRecoveryCooldownSec,\n\t\t\tMaxConcurrencyPerExtension: DefaultExtensionsMaxConcurrency,\n\t\t\tConflictPolicy: DefaultExtensionsConflictPolicy,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SYSTEM_SANDBOX_MODE\")); value != \"\" {\n\t\tcfg.SystemSandboxMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tswitch strings.ToLower(strings.TrimSpace(cfg.SystemSandboxMode)) {\n\tcase \"\", \"off\":\n\t\tcfg.SystemSandboxMode = \"off\"\n\tcase \"best_effort\":\n\t\tcfg.SystemSandboxMode = \"best_effort\"\n\tcase \"required\":\n\t\tcfg.SystemSandboxMode = \"required\"\n\tdefault:\n\t\treturn errors.New(\"system_sandbox_mode must be one of off, best_effort, required\")\n\t}\n\tif cfg.SystemSandboxMode != \"off\" && !cfg.SandboxEnabled {\n\t\treturn errors.New(\"system_sandbox_mode requires sandbox_enabled=true\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeExtensionsConfig(&cfg.Extensions); err != nil {\n\t\treturn err\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeExtensionsConfig(cfg *ExtensionsConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.AutoLoad == nil {\n\t\tcfg.AutoLoad = boolPtr(true)\n\t}\n\tif cfg.HealthCheckIntervalSec <= 0 {\n\t\tcfg.HealthCheckIntervalSec = DefaultExtensionsHealthCheckInterval\n\t}\n\tif cfg.FailureThreshold < 1 {\n\t\tcfg.FailureThreshold = DefaultExtensionsFailureThreshold\n\t}\n\tif cfg.RecoveryCooldownSec < 1 {\n\t\tcfg.RecoveryCooldownSec = DefaultExtensionsRecoveryCooldownSec\n\t}\n\tif cfg.MaxConcurrencyPerExtension < 1 {\n\t\tcfg.MaxConcurrencyPerExtension = DefaultExtensionsMaxConcurrency\n\t}\n\tcfg.ConflictPolicy = strings.ToLower(strings.TrimSpace(cfg.ConflictPolicy))\n\tif cfg.ConflictPolicy == \"\" {\n\t\tcfg.ConflictPolicy = DefaultExtensionsConflictPolicy\n\t}\n\tswitch cfg.ConflictPolicy {\n\tcase \"reject\", \"first_wins\", \"last_wins\":\n\tdefault:\n\t\treturn fmt.Errorf(\"extensions.conflict_policy must be one of reject|first_wins|last_wins\")\n\t}\n\n\tif len(cfg.Sources) == 0 {\n\t\tcfg.Sources = []string{\"skills\", \"mcp\"}\n\t\treturn nil\n\t}\n\tnormalized := make([]string, 0, len(cfg.Sources))\n\tseen := map[string]struct{}{}\n\tfor _, source := range cfg.Sources {\n\t\tsource = strings.ToLower(strings.TrimSpace(source))\n\t\tswitch source {\n\t\tcase \"skills\", \"mcp\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"extensions.sources contains unsupported value %q\", source)\n\t\t}\n\t\tif _, ok := seen[source]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[source] = struct{}{}\n\t\tnormalized = append(normalized, source)\n\t}\n\tif len(normalized) == 0 {\n\t\treturn errors.New(\"extensions.sources cannot be empty\")\n\t}\n\tcfg.Sources = normalized\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n\nfunc boolPtr(value bool) *bool {\n\tout := value\n\treturn &out\n}\n"}, "d7c307b765507451dc188c93847de867860dbc3f": {"t": 1785749510.9573972, "n": 36255, "h": "7b756203f1839bae", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n\tDefaultExtensionsHealthCheckInterval = 30\n\tDefaultExtensionsFailureThreshold = 3\n\tDefaultExtensionsRecoveryCooldownSec = 30\n\tDefaultExtensionsMaxConcurrency = 4\n\tDefaultExtensionsConflictPolicy = \"reject\"\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tExtensions ExtensionsConfig `json:\"extensions\"`\n\tMCP MCPConfig `json:\"mcp\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExtensionsConfig struct {\n\tSources []string `json:\"sources\"`\n\tAutoLoad *bool `json:\"auto_load,omitempty\"`\n\tHealthCheckIntervalSec int `json:\"health_check_interval_sec\"`\n\tFailureThreshold int `json:\"failure_threshold\"`\n\tRecoveryCooldownSec int `json:\"recovery_cooldown_sec\"`\n\tMaxConcurrencyPerExtension int `json:\"max_concurrency_per_extension\"`\n\tConflictPolicy string `json:\"conflict_policy\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc (cfg ExtensionsConfig) AutoLoadValue() bool {\n\tif cfg.AutoLoad == nil {\n\t\treturn true\n\t}\n\treturn *cfg.AutoLoad\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tExtensions: ExtensionsConfig{\n\t\t\tSources: []string{\"skills\", \"mcp\"},\n\t\t\tAutoLoad: boolPtr(true),\n\t\t\tHealthCheckIntervalSec: DefaultExtensionsHealthCheckInterval,\n\t\t\tFailureThreshold: DefaultExtensionsFailureThreshold,\n\t\t\tRecoveryCooldownSec: DefaultExtensionsRecoveryCooldownSec,\n\t\t\tMaxConcurrencyPerExtension: DefaultExtensionsMaxConcurrency,\n\t\t\tConflictPolicy: DefaultExtensionsConflictPolicy,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tExtensions: ExtensionsConfig{\n\t\t\tSources: []string{\"skills\", \"mcp\"},\n\t\t\tAutoLoad: boolPtr(true),\n\t\t\tHealthCheckIntervalSec: DefaultExtensionsHealthCheckInterval,\n\t\t\tFailureThreshold: DefaultExtensionsFailureThreshold,\n\t\t\tRecoveryCooldownSec: DefaultExtensionsRecoveryCooldownSec,\n\t\t\tMaxConcurrencyPerExtension: DefaultExtensionsMaxConcurrency,\n\t\t\tConflictPolicy: DefaultExtensionsConflictPolicy,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeExtensionsConfig(&cfg.Extensions); err != nil {\n\t\treturn err\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeExtensionsConfig(cfg *ExtensionsConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.AutoLoad == nil {\n\t\tcfg.AutoLoad = boolPtr(true)\n\t}\n\tif cfg.HealthCheckIntervalSec <= 0 {\n\t\tcfg.HealthCheckIntervalSec = DefaultExtensionsHealthCheckInterval\n\t}\n\tif cfg.FailureThreshold < 1 {\n\t\tcfg.FailureThreshold = DefaultExtensionsFailureThreshold\n\t}\n\tif cfg.RecoveryCooldownSec < 1 {\n\t\tcfg.RecoveryCooldownSec = DefaultExtensionsRecoveryCooldownSec\n\t}\n\tif cfg.MaxConcurrencyPerExtension < 1 {\n\t\tcfg.MaxConcurrencyPerExtension = DefaultExtensionsMaxConcurrency\n\t}\n\tcfg.ConflictPolicy = strings.ToLower(strings.TrimSpace(cfg.ConflictPolicy))\n\tif cfg.ConflictPolicy == \"\" {\n\t\tcfg.ConflictPolicy = DefaultExtensionsConflictPolicy\n\t}\n\tswitch cfg.ConflictPolicy {\n\tcase \"reject\", \"first_wins\", \"last_wins\":\n\tdefault:\n\t\treturn fmt.Errorf(\"extensions.conflict_policy must be one of reject|first_wins|last_wins\")\n\t}\n\n\tif len(cfg.Sources) == 0 {\n\t\tcfg.Sources = []string{\"skills\", \"mcp\"}\n\t\treturn nil\n\t}\n\tnormalized := make([]string, 0, len(cfg.Sources))\n\tseen := map[string]struct{}{}\n\tfor _, source := range cfg.Sources {\n\t\tsource = strings.ToLower(strings.TrimSpace(source))\n\t\tswitch source {\n\t\tcase \"skills\", \"mcp\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"extensions.sources contains unsupported value %q\", source)\n\t\t}\n\t\tif _, ok := seen[source]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[source] = struct{}{}\n\t\tnormalized = append(normalized, source)\n\t}\n\tif len(normalized) == 0 {\n\t\treturn errors.New(\"extensions.sources cannot be empty\")\n\t}\n\tcfg.Sources = normalized\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n\nfunc boolPtr(value bool) *bool {\n\tout := value\n\treturn &out\n}\n"}, "ffd66190f6addc7784ecb021f543b233d15ac98b": {"t": 1785749512.3602455, "n": 32704, "h": "4b724c85231f16d9", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"mcp\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "ef90aa9a985769c18a14bb2fe1ea0af6edbf7dfb": {"t": 1785749513.7378266, "n": 27056, "h": "6fc331950a824a5f", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\tplanpkg \"bytemind/internal/plan\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n\tDefaultMCPSyncTTLSeconds = 30\n\tDefaultMCPStartupTimeoutSeconds = 20\n\tDefaultMCPCallTimeoutSeconds = 60\n\tDefaultMCPMaxConcurrency = 4\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n\tMCP MCPConfig `json:\"mcp\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype MCPConfig struct {\n\tEnabled bool `json:\"enabled\"`\n\tSyncTTLSeconds int `json:\"sync_ttl_s\"`\n\tServers []MCPServerConfig `json:\"servers\"`\n}\n\ntype MCPServerConfig struct {\n\tID string `json:\"id\"`\n\tName string `json:\"name\"`\n\tEnabled *bool `json:\"enabled,omitempty\"`\n\tTransport MCPTransportConfig `json:\"transport\"`\n\tAutoStart *bool `json:\"auto_start,omitempty\"`\n\tStartupTimeoutSeconds int `json:\"startup_timeout_s\"`\n\tCallTimeoutSeconds int `json:\"call_timeout_s\"`\n\tMaxConcurrency int `json:\"max_concurrency\"`\n\tToolOverrides []MCPToolOverrideConfig `json:\"tool_overrides\"`\n\tProtocolVersion string `json:\"protocol_version\"`\n\tProtocolVersions []string `json:\"protocol_versions\"`\n}\n\ntype MCPTransportConfig struct {\n\tType string `json:\"type\"`\n\tCommand string `json:\"command\"`\n\tArgs []string `json:\"args\"`\n\tEnv map[string]string `json:\"env\"`\n\tCWD string `json:\"cwd\"`\n}\n\ntype MCPToolOverrideConfig struct {\n\tToolName string `json:\"tool_name\"`\n\tSafetyClass string `json:\"safety_class,omitempty\"`\n\tReadOnly *bool `json:\"read_only,omitempty\"`\n\tDestructive *bool `json:\"destructive,omitempty\"`\n\tAllowedModes []string `json:\"allowed_modes,omitempty\"`\n\tDefaultTimeoutS int `json:\"default_timeout_s,omitempty\"`\n\tMaxTimeoutS int `json:\"max_timeout_s,omitempty\"`\n\tMaxResultChars int `json:\"max_result_chars,omitempty\"`\n}\n\nfunc (s MCPServerConfig) EnabledValue() bool {\n\tif s.Enabled == nil {\n\t\treturn true\n\t}\n\treturn *s.Enabled\n}\n\nfunc (s MCPServerConfig) AutoStartValue() bool {\n\tif s.AutoStart == nil {\n\t\treturn true\n\t}\n\treturn *s.AutoStart\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t\tMCP: MCPConfig{\n\t\t\tEnabled: false,\n\t\t\tSyncTTLSeconds: DefaultMCPSyncTTLSeconds,\n\t\t\tServers: nil,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\tif err := normalizeMCPConfig(&cfg.MCP); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc normalizeMCPConfig(cfg *MCPConfig) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tif cfg.SyncTTLSeconds <= 0 {\n\t\tcfg.SyncTTLSeconds = DefaultMCPSyncTTLSeconds\n\t}\n\tif len(cfg.Servers) == 0 {\n\t\tcfg.Servers = nil\n\t\treturn nil\n\t}\n\tnormalized := make([]MCPServerConfig, 0, len(cfg.Servers))\n\tnormalizedSources := make(map[string]string, len(cfg.Servers))\n\tfor _, server := range cfg.Servers {\n\t\toriginalID := strings.TrimSpace(server.ID)\n\t\tserver.ID = normalizeMCPServerID(server.ID)\n\t\tif server.ID == \"\" {\n\t\t\treturn errors.New(\"mcp.servers contains an empty server id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[server.ID]; exists {\n\t\t\treturn fmt.Errorf(\"mcp.servers has duplicate server id after normalization: %q (from %q and %q)\", server.ID, existingSource, originalID)\n\t\t}\n\t\tnormalizedSources[server.ID] = originalID\n\t\tserver.Name = strings.TrimSpace(server.Name)\n\t\tif server.Name == \"\" {\n\t\t\tserver.Name = server.ID\n\t\t}\n\t\tif server.Enabled == nil {\n\t\t\tvalue := true\n\t\t\tserver.Enabled = &value\n\t\t}\n\t\tif server.AutoStart == nil {\n\t\t\tvalue := true\n\t\t\tserver.AutoStart = &value\n\t\t}\n\t\tserver.Transport.Type = strings.ToLower(strings.TrimSpace(server.Transport.Type))\n\t\tif server.Transport.Type == \"\" {\n\t\t\tserver.Transport.Type = \"stdio\"\n\t\t}\n\t\tif server.Transport.Type != \"stdio\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q uses unsupported transport.type %q (expected stdio)\", server.ID, server.Transport.Type)\n\t\t}\n\t\tserver.Transport.Command = strings.TrimSpace(server.Transport.Command)\n\t\tserver.Transport.CWD = strings.TrimSpace(server.Transport.CWD)\n\t\tif server.EnabledValue() && server.Transport.Command == \"\" {\n\t\t\treturn fmt.Errorf(\"mcp server %q requires transport.command for stdio mode\", server.ID)\n\t\t}\n\t\tif server.Transport.Args == nil {\n\t\t\tserver.Transport.Args = []string{}\n\t\t}\n\t\tfor i, arg := range server.Transport.Args {\n\t\t\tserver.Transport.Args[i] = strings.TrimSpace(arg)\n\t\t}\n\t\tcleanEnv := map[string]string{}\n\t\tfor key, value := range server.Transport.Env {\n\t\t\tkey = strings.TrimSpace(key)\n\t\t\tif key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcleanEnv[key] = strings.TrimSpace(value)\n\t\t}\n\t\tserver.Transport.Env = cleanEnv\n\t\tif server.StartupTimeoutSeconds <= 0 {\n\t\t\tserver.StartupTimeoutSeconds = DefaultMCPStartupTimeoutSeconds\n\t\t}\n\t\tif server.CallTimeoutSeconds <= 0 {\n\t\t\tserver.CallTimeoutSeconds = DefaultMCPCallTimeoutSeconds\n\t\t}\n\t\tif server.MaxConcurrency < 1 {\n\t\t\tserver.MaxConcurrency = DefaultMCPMaxConcurrency\n\t\t}\n\t\tserver.ProtocolVersion = strings.TrimSpace(server.ProtocolVersion)\n\t\tserver.ProtocolVersions = normalizeMCPProtocolVersions(server.ProtocolVersion, server.ProtocolVersions)\n\t\tnormalizedOverrides, err := normalizeMCPToolOverrides(server.ID, server.ToolOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tserver.ToolOverrides = normalizedOverrides\n\t\tnormalized = append(normalized, server)\n\t}\n\tcfg.Servers = normalized\n\treturn nil\n}\n\nfunc normalizeMCPServerID(raw string) string {\n\traw = strings.TrimSpace(strings.ToLower(raw))\n\tif raw == \"\" {\n\t\treturn \"\"\n\t}\n\treplacer := strings.NewReplacer(\" \", \"-\", \"/\", \"-\", \"\\\\\", \"-\", \":\", \"-\", \".\", \"-\")\n\traw = replacer.Replace(raw)\n\traw = strings.Trim(raw, \"-_\")\n\treturn raw\n}\n\nfunc normalizeMCPProtocolVersions(primary string, extras []string) []string {\n\tversions := make([]string, 0, 1+len(extras))\n\tif strings.TrimSpace(primary) != \"\" {\n\t\tversions = append(versions, strings.TrimSpace(primary))\n\t}\n\tversions = append(versions, extras...)\n\tnormalized := make([]string, 0, len(versions))\n\tseen := map[string]struct{}{}\n\tfor _, version := range versions {\n\t\tversion = strings.TrimSpace(version)\n\t\tif version == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := seen[version]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tseen[version] = struct{}{}\n\t\tnormalized = append(normalized, version)\n\t}\n\treturn normalized\n}\n\nfunc normalizeMCPToolOverrides(serverID string, overrides []MCPToolOverrideConfig) ([]MCPToolOverrideConfig, error) {\n\tif len(overrides) == 0 {\n\t\treturn nil, nil\n\t}\n\tnormalized := make([]MCPToolOverrideConfig, 0, len(overrides))\n\tseen := map[string]struct{}{}\n\tfor _, override := range overrides {\n\t\toverride.ToolName = strings.TrimSpace(override.ToolName)\n\t\tif override.ToolName == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override with empty tool_name\", serverID)\n\t\t}\n\t\ttoolKey := strings.ToLower(override.ToolName)\n\t\tif _, exists := seen[toolKey]; exists {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has duplicate tool override for %q\", serverID, override.ToolName)\n\t\t}\n\t\tseen[toolKey] = struct{}{}\n\t\toverride.SafetyClass = strings.ToLower(strings.TrimSpace(override.SafetyClass))\n\t\tnormalizedModes := make([]string, 0, len(override.AllowedModes))\n\t\tmodeSeen := map[planpkg.AgentMode]struct{}{}\n\t\tfor _, mode := range override.AllowedModes {\n\t\t\tnormalizedMode := planpkg.NormalizeMode(strings.TrimSpace(mode))\n\t\t\tif normalizedMode == \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with invalid allowed mode %q\", serverID, override.ToolName, mode)\n\t\t\t}\n\t\t\tif _, exists := modeSeen[normalizedMode]; exists {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmodeSeen[normalizedMode] = struct{}{}\n\t\t\tnormalizedModes = append(normalizedModes, string(normalizedMode))\n\t\t}\n\t\toverride.AllowedModes = normalizedModes\n\t\tif override.DefaultTimeoutS < 0 || override.MaxTimeoutS < 0 || override.MaxResultChars < 0 {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q has tool override %q with negative timeout/result limits\", serverID, override.ToolName)\n\t\t}\n\t\tif err := validateMCPToolOverride(override); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"mcp server %q tool override %q is invalid: %w\", serverID, override.ToolName, err)\n\t\t}\n\t\tnormalized = append(normalized, override)\n\t}\n\treturn normalized, nil\n}\n\nfunc validateMCPToolOverride(override MCPToolOverrideConfig) error {\n\tswitch override.SafetyClass {\n\tcase \"\", \"safe\", \"moderate\", \"sensitive\", \"destructive\":\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported safety_class %q\", override.SafetyClass)\n\t}\n\n\tif override.ReadOnly != nil && override.Destructive != nil && *override.ReadOnly && *override.Destructive {\n\t\treturn fmt.Errorf(\"read_only and destructive cannot both be true\")\n\t}\n\tif override.DefaultTimeoutS > 0 && override.MaxTimeoutS > 0 && override.DefaultTimeoutS > override.MaxTimeoutS {\n\t\treturn fmt.Errorf(\"default_timeout_s must be <= max_timeout_s\")\n\t}\n\treturn nil\n}\n"}, "87266161a4497f8c75e594be0ddfed3434f85147": {"t": 1785749514.9714344, "n": 23929, "h": "577a4edc00fc57cd", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, value)\n\t}\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}, "e75acf7cd1ca26ab5b13b8255425cd0ffaa92466": {"t": 1785749516.2064972, "n": 24079, "h": "314479fa119b2d31", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tseen := make(map[string]struct{}, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, exists := seen[value]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[value] = struct{}{}\n\t\tout = append(out, value)\n\t}\n\tsort.Strings(out)\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}, "bbeabc47260eaf34e82b170be3547071988bb820": {"t": 1785749517.421251, "n": 23665, "h": "bb2e6a3ec97bc3f8", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommandTokens := strings.Fields(strings.TrimSpace(rule.Command))\n\t\tif len(commandTokens) == 0 {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tcommand := commandTokens[0]\n\t\tpatternInputs := make([]string, 0, len(commandTokens)-1+len(rule.ArgsPattern))\n\t\tif len(commandTokens) > 1 {\n\t\t\tpatternInputs = append(patternInputs, commandTokens[1:]...)\n\t\t}\n\t\tpatternInputs = append(patternInputs, rule.ArgsPattern...)\n\t\tpatterns := normalizeStringList(patternInputs)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tseen := make(map[string]struct{}, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, exists := seen[value]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[value] = struct{}{}\n\t\tout = append(out, value)\n\t}\n\tsort.Strings(out)\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}, "53ede02f7382275491e23d0bd7c88fa3654c6f3a": {"t": 1785749518.7771149, "n": 23367, "h": "1e1bedc603408635", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tSandboxEnabled bool `json:\"sandbox_enabled\"`\n\tWritableRoots []string `json:\"writable_roots\"`\n\tExecAllowlist []ExecAllowRule `json:\"exec_allowlist\"`\n\tNetworkAllowlist []NetworkAllowRule `json:\"network_allowlist\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\ntype ExecAllowRule struct {\n\tCommand string `json:\"command\"`\n\tArgsPattern []string `json:\"args_pattern\"`\n}\n\ntype NetworkAllowRule struct {\n\tHost string `json:\"host\"`\n\tPort int `json:\"port\"`\n\tScheme string `json:\"scheme\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif err := normalizeWritableRoots(workspace, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tSandboxEnabled: false,\n\t\tWritableRoots: []string{},\n\t\tExecAllowlist: []ExecAllowRule{},\n\t\tNetworkAllowlist: []NetworkAllowRule{},\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_SANDBOX_ENABLED\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.SandboxEnabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_WRITABLE_ROOTS\")); value != \"\" {\n\t\tcfg.WritableRoots = splitPathList(value)\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tif err := normalizeSandboxPolicy(cfg); err != nil {\n\t\treturn err\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeWritableRoots(workspace string, cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\tabsWorkspace, err := filepath.Abs(strings.TrimSpace(workspace))\n\tif err != nil {\n\t\treturn err\n\t}\n\tabsWorkspace = filepath.Clean(absWorkspace)\n\tworkspaceKey := normalizePathKey(absWorkspace)\n\n\tif len(cfg.WritableRoots) == 0 {\n\t\tcfg.WritableRoots = []string{}\n\t\treturn nil\n\t}\n\tseen := map[string]struct{}{workspaceKey: {}}\n\tnormalized := make([]string, 0, len(cfg.WritableRoots))\n\tfor _, root := range cfg.WritableRoots {\n\t\troot = strings.TrimSpace(root)\n\t\tif root == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !filepath.IsAbs(root) {\n\t\t\troot = filepath.Join(absWorkspace, root)\n\t\t}\n\t\tabsRoot, err := filepath.Abs(root)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tabsRoot = filepath.Clean(absRoot)\n\t\tkey := normalizePathKey(absRoot)\n\t\tif _, exists := seen[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[key] = struct{}{}\n\t\tnormalized = append(normalized, absRoot)\n\t}\n\tcfg.WritableRoots = normalized\n\treturn nil\n}\n\nfunc normalizeSandboxPolicy(cfg *Config) error {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\tnormalizedExec := make([]ExecAllowRule, 0, len(cfg.ExecAllowlist))\n\tseenExec := make(map[string]struct{}, len(cfg.ExecAllowlist))\n\tfor _, rule := range cfg.ExecAllowlist {\n\t\tcommand := strings.TrimSpace(rule.Command)\n\t\tif command == \"\" {\n\t\t\treturn errors.New(\"exec_allowlist.command cannot be empty\")\n\t\t}\n\t\tpatterns := normalizeStringList(rule.ArgsPattern)\n\t\tkey := strings.ToLower(command) + \"\\x00\" + strings.Join(patterns, \"\\x00\")\n\t\tif _, exists := seenExec[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenExec[key] = struct{}{}\n\t\tnormalizedExec = append(normalizedExec, ExecAllowRule{\n\t\t\tCommand: command,\n\t\t\tArgsPattern: patterns,\n\t\t})\n\t}\n\tsort.Slice(normalizedExec, func(i, j int) bool {\n\t\ta := strings.ToLower(normalizedExec[i].Command)\n\t\tb := strings.ToLower(normalizedExec[j].Command)\n\t\tif a != b {\n\t\t\treturn a < b\n\t\t}\n\t\treturn strings.Join(normalizedExec[i].ArgsPattern, \"\\x00\") < strings.Join(normalizedExec[j].ArgsPattern, \"\\x00\")\n\t})\n\tcfg.ExecAllowlist = normalizedExec\n\n\tnormalizedNetwork := make([]NetworkAllowRule, 0, len(cfg.NetworkAllowlist))\n\tseenNetwork := make(map[string]struct{}, len(cfg.NetworkAllowlist))\n\tfor _, rule := range cfg.NetworkAllowlist {\n\t\thost := strings.ToLower(strings.TrimSpace(rule.Host))\n\t\tscheme := strings.ToLower(strings.TrimSpace(rule.Scheme))\n\t\tif host == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.host cannot be empty\")\n\t\t}\n\t\tif scheme == \"\" {\n\t\t\treturn errors.New(\"network_allowlist.scheme cannot be empty\")\n\t\t}\n\t\tif rule.Port < 1 || rule.Port > 65535 {\n\t\t\treturn errors.New(\"network_allowlist.port must be between 1 and 65535\")\n\t\t}\n\t\tkey := host + \"\\x00\" + strconv.Itoa(rule.Port) + \"\\x00\" + scheme\n\t\tif _, exists := seenNetwork[key]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseenNetwork[key] = struct{}{}\n\t\tnormalizedNetwork = append(normalizedNetwork, NetworkAllowRule{\n\t\t\tHost: host,\n\t\t\tPort: rule.Port,\n\t\t\tScheme: scheme,\n\t\t})\n\t}\n\tsort.Slice(normalizedNetwork, func(i, j int) bool {\n\t\ta := normalizedNetwork[i]\n\t\tb := normalizedNetwork[j]\n\t\tif a.Host != b.Host {\n\t\t\treturn a.Host < b.Host\n\t\t}\n\t\tif a.Port != b.Port {\n\t\t\treturn a.Port < b.Port\n\t\t}\n\t\treturn a.Scheme < b.Scheme\n\t})\n\tcfg.NetworkAllowlist = normalizedNetwork\n\treturn nil\n}\n\nfunc normalizeStringList(values []string) []string {\n\tif len(values) == 0 {\n\t\treturn []string{}\n\t}\n\tout := make([]string, 0, len(values))\n\tseen := make(map[string]struct{}, len(values))\n\tfor _, value := range values {\n\t\tvalue = strings.TrimSpace(value)\n\t\tif value == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, exists := seen[value]; exists {\n\t\t\tcontinue\n\t\t}\n\t\tseen[value] = struct{}{}\n\t\tout = append(out, value)\n\t}\n\tsort.Strings(out)\n\treturn out\n}\n\nfunc splitPathList(raw string) []string {\n\traw = strings.TrimSpace(raw)\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\tparts := strings.Split(raw, string(os.PathListSeparator))\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, part)\n\t}\n\treturn result\n}\n\nfunc normalizePathKey(path string) string {\n\tpath = filepath.Clean(strings.TrimSpace(path))\n\tif runtime.GOOS == \"windows\" {\n\t\treturn strings.ToLower(path)\n\t}\n\treturn path\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}, "56d5d49403af1093e6e360cd659dd5c502d04ed1": {"t": 1785749520.04792, "n": 18282, "h": "d968e4a640e46b87", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tUpdateCheck UpdateCheckConfig `json:\"update_check\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype UpdateCheckConfig struct {\n\tEnabled bool `json:\"enabled\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tUpdateCheck: UpdateCheckConfig{\n\t\t\tEnabled: true,\n\t\t},\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_UPDATE_CHECK\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.UpdateCheck.Enabled = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}, "9aa4026fd5c0ce285448aa6ff784c831623747a8": {"t": 1785749521.1862547, "n": 17865, "h": "88d3df71d9bf9a3a", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tApprovalMode string `json:\"approval_mode\"`\n\tAwayPolicy string `json:\"away_policy\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tApprovalMode: \"interactive\",\n\t\tAwayPolicy: \"auto_deny_continue\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_MODE\")); value != \"\" {\n\t\tcfg.ApprovalMode = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_AWAY_POLICY\")); value != \"\" {\n\t\tcfg.AwayPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tswitch strings.TrimSpace(cfg.ApprovalMode) {\n\tcase \"\", \"interactive\":\n\t\tcfg.ApprovalMode = \"interactive\"\n\tcase \"away\":\n\tdefault:\n\t\treturn errors.New(\"approval_mode must be one of interactive, away\")\n\t}\n\tswitch strings.TrimSpace(cfg.AwayPolicy) {\n\tcase \"\", \"auto_deny_continue\":\n\t\tcfg.AwayPolicy = \"auto_deny_continue\"\n\tcase \"fail_fast\":\n\tdefault:\n\t\treturn errors.New(\"away_policy must be one of auto_deny_continue, fail_fast\")\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}, "c2af48420eaa8d28103486b2372eef2d03db5d74": {"t": 1785749522.3230472, "n": 16944, "h": "2cbadb0536ea01cb", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}, "b46ecd520ef166922de47b978ce7d74c4e466e0c": {"t": 1785749523.5702732, "n": 14085, "h": "c9945f53a7b9722d", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultTokenQuota = 300000\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: DefaultTokenQuota,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = DefaultTokenQuota\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}, "b59108d6dc76e4f4422fcd6516ecf9c3edf5d4a4": {"t": 1785749525.2561498, "n": 14001, "h": "aa0218ba6310bbe8", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: 300000,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: 5000,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = 5000\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}, "ff8ff0327b0171d7ead027388440f0d9cb3b5e4f": {"t": 1785749526.5685465, "n": 16858, "h": "3218e37868dca969", "c": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tenvBytemindHome = \"BYTEMIND_HOME\"\n\tdefaultHomeDir = \".bytemind\"\n\tdefaultModelID = \"gpt-5.4-mini\"\n)\n\nconst (\n\tDefaultContextBudgetWarningRatio = 0.85\n\tDefaultContextBudgetCriticalRatio = 0.95\n\tDefaultContextBudgetMaxReactiveRetry = 1\n)\n\ntype Config struct {\n\tProvider ProviderConfig `json:\"provider\"`\n\tProviderRuntime ProviderRuntimeConfig `json:\"provider_runtime\"`\n\tApprovalPolicy string `json:\"approval_policy\"`\n\tMaxIterations int `json:\"max_iterations\"`\n\tStream bool `json:\"stream\"`\n\tTokenQuota int `json:\"token_quota\"`\n\tTokenUsage TokenUsageConfig `json:\"token_usage\"`\n\tContextBudget ContextBudgetConfig `json:\"context_budget\"`\n}\n\ntype ProviderConfig struct {\n\tType string `json:\"type\"`\n\tAutoDetectType bool `json:\"auto_detect_type\"`\n\tBaseURL string `json:\"base_url\"`\n\tAPIPath string `json:\"api_path\"`\n\tModel string `json:\"model\"`\n\tAPIKey string `json:\"api_key\"`\n\tAPIKeyEnv string `json:\"api_key_env\"`\n\tAuthHeader string `json:\"auth_header\"`\n\tAuthScheme string `json:\"auth_scheme\"`\n\tExtraHeaders map[string]string `json:\"extra_headers\"`\n\tAnthropicVersion string `json:\"anthropic_version\"`\n}\n\ntype TokenUsageConfig struct {\n\tStorageType string `json:\"storage_type\"`\n\tStoragePath string `json:\"storage_path\"`\n\tBackupInterval string `json:\"backup_interval\"`\n\tMaxSessions int `json:\"max_sessions\"`\n\tAlertThreshold int64 `json:\"alert_threshold\"`\n\tEnableRealtime bool `json:\"enable_realtime\"`\n\tRetentionDays int `json:\"retention_days\"`\n\tMonitorInterval string `json:\"monitor_interval\"`\n\tDatabaseDriver string `json:\"database_driver\"`\n}\n\ntype ContextBudgetConfig struct {\n\tWarningRatio float64 `json:\"warning_ratio\"`\n\tCriticalRatio float64 `json:\"critical_ratio\"`\n\tMaxReactiveRetry int `json:\"max_reactive_retry\"`\n}\n\nfunc Default(workspace string) Config {\n\treturn Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: 5000,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n}\n\nfunc Load(workspace, configPath string) (Config, error) {\n\tcfg := Default(workspace)\n\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tpath, err := resolveConfigPath(workspace, configPath)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tif err := mergeConfigFromFile(path, &cfg); err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t} else {\n\t\tif userConfig, err := resolveUserConfigPath(); err != nil {\n\t\t\treturn cfg, err\n\t\t} else if userConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(userConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\n\t\tif projectConfig := resolveProjectConfigPath(workspace); projectConfig != \"\" {\n\t\t\tif err := mergeConfigFromFile(projectConfig, &cfg); err != nil {\n\t\t\t\treturn cfg, err\n\t\t\t}\n\t\t}\n\t}\n\n\tapplyEnv(&cfg)\n\tif err := normalize(&cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nfunc (p ProviderConfig) ResolveAPIKey() string {\n\tif strings.TrimSpace(p.APIKey) != \"\" {\n\t\treturn strings.TrimSpace(p.APIKey)\n\t}\n\tif env := strings.TrimSpace(p.APIKeyEnv); env != \"\" {\n\t\treturn strings.TrimSpace(os.Getenv(env))\n\t}\n\treturn strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\"))\n}\n\nfunc ResolveHomeDir() (string, error) {\n\tif override := strings.TrimSpace(os.Getenv(envBytemindHome)); override != \"\" {\n\t\treturn filepath.Abs(override)\n\t}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(home, defaultHomeDir), nil\n}\n\nfunc EnsureHomeLayout() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirs := []string{\n\t\thome,\n\t\tfilepath.Join(home, \"sessions\"),\n\t\tfilepath.Join(home, \"logs\"),\n\t\tfilepath.Join(home, \"cache\"),\n\t\tfilepath.Join(home, \"auth\"),\n\t\tfilepath.Join(home, \"migrations\"),\n\t}\n\tfor _, dir := range dirs {\n\t\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\tif err := ensureDefaultConfigFile(home); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn home, nil\n}\n\nfunc ensureDefaultConfigFile(home string) error {\n\tpath := filepath.Join(home, \"config.json\")\n\tif info, err := os.Stat(path); err == nil {\n\t\tif info.IsDir() {\n\t\t\treturn errors.New(\"home config path is a directory\")\n\t\t}\n\t\treturn nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\tcfg := Config{\n\t\tProvider: ProviderConfig{\n\t\t\tType: \"openai-compatible\",\n\t\t\tBaseURL: \"https://api.openai.com/v1\",\n\t\t\tModel: defaultModelID,\n\t\t\tAPIKeyEnv: \"BYTEMIND_API_KEY\",\n\t\t\tAnthropicVersion: \"2023-06-01\",\n\t\t},\n\t\tApprovalPolicy: \"on-request\",\n\t\tMaxIterations: 32,\n\t\tStream: true,\n\t\tTokenQuota: 5000,\n\t\tTokenUsage: TokenUsageConfig{\n\t\t\tStorageType: \"file\",\n\t\t\tStoragePath: \".bytemind/token_usage.json\",\n\t\t\tBackupInterval: \"1m\",\n\t\t\tMaxSessions: 10000,\n\t\t\tAlertThreshold: 1000000,\n\t\t\tEnableRealtime: true,\n\t\t\tRetentionDays: 30,\n\t\t\tMonitorInterval: \"30s\",\n\t\t\tDatabaseDriver: \"sqlite3\",\n\t\t},\n\t\tContextBudget: ContextBudgetConfig{\n\t\t\tWarningRatio: DefaultContextBudgetWarningRatio,\n\t\t\tCriticalRatio: DefaultContextBudgetCriticalRatio,\n\t\t\tMaxReactiveRetry: DefaultContextBudgetMaxReactiveRetry,\n\t\t},\n\t}\n\n\tdata, err := json.MarshalIndent(cfg, \"\", \" \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata = append(data, '\\n')\n\treturn os.WriteFile(path, data, 0o644)\n}\n\nfunc resolveConfigPath(workspace, explicit string) (string, error) {\n\tif strings.TrimSpace(explicit) != \"\" {\n\t\treturn filepath.Abs(explicit)\n\t}\n\n\tif project := resolveProjectConfigPath(workspace); project != \"\" {\n\t\treturn project, nil\n\t}\n\tif user, err := resolveUserConfigPath(); err == nil && user != \"\" {\n\t\treturn user, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc resolveProjectConfigPath(workspace string) string {\n\tcandidates := []string{\n\t\tfilepath.Join(workspace, \".bytemind\", \"config.json\"),\n\t}\n\tfor _, candidate := range candidates {\n\t\tif _, err := os.Stat(candidate); err == nil {\n\t\t\treturn candidate\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc resolveUserConfigPath() (string, error) {\n\thome, err := ResolveHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcandidate := filepath.Join(home, \"config.json\")\n\tif _, err := os.Stat(candidate); err == nil {\n\t\treturn candidate, nil\n\t}\n\treturn \"\", nil\n}\n\nfunc mergeConfigFromFile(path string, cfg *Config) error {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc applyEnv(cfg *Config) {\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_TYPE\")); value != \"\" {\n\t\tcfg.Provider.Type = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_PROVIDER_AUTO_DETECT_TYPE\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Provider.AutoDetectType = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_BASE_URL\")); value != \"\" {\n\t\tcfg.Provider.BaseURL = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_MODEL\")); value != \"\" {\n\t\tcfg.Provider.Model = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY\")); value != \"\" {\n\t\tcfg.Provider.APIKey = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_API_KEY_ENV\")); value != \"\" {\n\t\tcfg.Provider.APIKeyEnv = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_APPROVAL_POLICY\")); value != \"\" {\n\t\tcfg.ApprovalPolicy = value\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_STREAM\")); value != \"\" {\n\t\tif parsed, err := strconv.ParseBool(value); err == nil {\n\t\t\tcfg.Stream = parsed\n\t\t}\n\t}\n\tif value := strings.TrimSpace(os.Getenv(\"BYTEMIND_TOKEN_QUOTA\")); value != \"\" {\n\t\tif parsed, err := strconv.Atoi(value); err == nil {\n\t\t\tcfg.TokenQuota = parsed\n\t\t}\n\t}\n}\n\nfunc normalize(cfg *Config) error {\n\tcfg.Provider.Type = normalizeProviderType(cfg.Provider.Type)\n\tif cfg.Provider.Type == \"\" {\n\t\tif cfg.Provider.AutoDetectType {\n\t\t\tcfg.Provider.Type = detectProviderType(cfg.Provider)\n\t\t} else {\n\t\t\tcfg.Provider.Type = \"openai-compatible\"\n\t\t}\n\t}\n\tif cfg.Provider.BaseURL == \"\" {\n\t\tcfg.Provider.BaseURL = defaultBaseURL(cfg.Provider.Type)\n\t}\n\tif strings.TrimSpace(cfg.Provider.BaseURL) == \"\" {\n\t\treturn errors.New(\"provider.base_url is required\")\n\t}\n\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\tcfg.Provider.Model = defaultModel(cfg.Provider.Type, cfg.Provider.BaseURL)\n\t\tif strings.TrimSpace(cfg.Provider.Model) == \"\" {\n\t\t\treturn errors.New(\"provider.model is required\")\n\t\t}\n\t}\n\tif cfg.Provider.APIKeyEnv == \"\" {\n\t\tcfg.Provider.APIKeyEnv = \"BYTEMIND_API_KEY\"\n\t}\n\tcfg.Provider.APIPath = strings.TrimSpace(cfg.Provider.APIPath)\n\tcfg.Provider.AuthHeader = strings.TrimSpace(cfg.Provider.AuthHeader)\n\tcfg.Provider.AuthScheme = strings.TrimSpace(cfg.Provider.AuthScheme)\n\tcfg.Provider.AnthropicVersion = strings.TrimSpace(cfg.Provider.AnthropicVersion)\n\tif cfg.Provider.Type == \"anthropic\" && cfg.Provider.AnthropicVersion == \"\" {\n\t\tcfg.Provider.AnthropicVersion = \"2023-06-01\"\n\t}\n\tif cfg.Provider.ExtraHeaders == nil {\n\t\tcfg.Provider.ExtraHeaders = map[string]string{}\n\t}\n\tif len(cfg.ProviderRuntime.Providers) == 0 {\n\t\tlegacy := LegacyProviderRuntimeConfig(cfg.Provider)\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultProvider = legacy.DefaultProvider\n\t\t}\n\t\tif strings.TrimSpace(cfg.ProviderRuntime.DefaultModel) == \"\" {\n\t\t\tcfg.ProviderRuntime.DefaultModel = legacy.DefaultModel\n\t\t}\n\t\tcfg.ProviderRuntime.Providers = legacy.Providers\n\t}\n\tcfg.ProviderRuntime.DefaultProvider = strings.ToLower(strings.TrimSpace(cfg.ProviderRuntime.DefaultProvider))\n\tcfg.ProviderRuntime.DefaultModel = strings.TrimSpace(cfg.ProviderRuntime.DefaultModel)\n\tif cfg.ProviderRuntime.DefaultModel == \"\" {\n\t\tcfg.ProviderRuntime.DefaultModel = cfg.Provider.Model\n\t}\n\tif cfg.ProviderRuntime.Providers == nil {\n\t\tcfg.ProviderRuntime.Providers = map[string]ProviderConfig{}\n\t}\n\tnormalizedProviders := make(map[string]ProviderConfig, len(cfg.ProviderRuntime.Providers))\n\tnormalizedSources := make(map[string]string, len(cfg.ProviderRuntime.Providers))\n\tfor id, providerCfg := range cfg.ProviderRuntime.Providers {\n\t\tnormalizedID := strings.ToLower(strings.TrimSpace(id))\n\t\tif normalizedID == \"\" {\n\t\t\treturn errors.New(\"provider_runtime.providers contains an empty provider id\")\n\t\t}\n\t\tif existingSource, exists := normalizedSources[normalizedID]; exists {\n\t\t\treturn fmt.Errorf(\"provider_runtime.providers has duplicate provider id after normalization: %q (from %q and %q)\", normalizedID, existingSource, id)\n\t\t}\n\t\tproviderCfg.Type = normalizeProviderType(providerCfg.Type)\n\t\tif providerCfg.Type == \"\" {\n\t\t\tif providerCfg.AutoDetectType {\n\t\t\t\tproviderCfg.Type = detectProviderType(providerCfg)\n\t\t\t} else {\n\t\t\t\tproviderCfg.Type = \"openai-compatible\"\n\t\t\t}\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.BaseURL) == \"\" {\n\t\t\tproviderCfg.BaseURL = defaultBaseURL(providerCfg.Type)\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.Model) == \"\" {\n\t\t\tproviderCfg.Model = cfg.ProviderRuntime.DefaultModel\n\t\t}\n\t\tif providerCfg.APIKeyEnv == \"\" {\n\t\t\tproviderCfg.APIKeyEnv = cfg.Provider.APIKeyEnv\n\t\t}\n\t\tif strings.TrimSpace(providerCfg.APIKey) == \"\" {\n\t\t\tproviderCfg.APIKey = cfg.Provider.APIKey\n\t\t}\n\t\tproviderCfg.APIPath = strings.TrimSpace(providerCfg.APIPath)\n\t\tproviderCfg.AuthHeader = strings.TrimSpace(providerCfg.AuthHeader)\n\t\tproviderCfg.AuthScheme = strings.TrimSpace(providerCfg.AuthScheme)\n\t\tproviderCfg.AnthropicVersion = strings.TrimSpace(providerCfg.AnthropicVersion)\n\t\tif providerCfg.Type == \"anthropic\" && providerCfg.AnthropicVersion == \"\" {\n\t\t\tproviderCfg.AnthropicVersion = \"2023-06-01\"\n\t\t}\n\t\tif providerCfg.ExtraHeaders == nil {\n\t\t\tproviderCfg.ExtraHeaders = map[string]string{}\n\t\t}\n\t\tnormalizedProviders[normalizedID] = providerCfg\n\t\tnormalizedSources[normalizedID] = id\n\t}\n\tcfg.ProviderRuntime.Providers = normalizedProviders\n\tfor key, value := range cfg.Provider.ExtraHeaders {\n\t\ttrimmedKey := strings.TrimSpace(key)\n\t\ttrimmedValue := strings.TrimSpace(value)\n\t\tif trimmedKey == \"\" || trimmedValue == \"\" {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcontinue\n\t\t}\n\t\tif trimmedKey != key {\n\t\t\tdelete(cfg.Provider.ExtraHeaders, key)\n\t\t\tcfg.Provider.ExtraHeaders[trimmedKey] = trimmedValue\n\t\t\tcontinue\n\t\t}\n\t\tcfg.Provider.ExtraHeaders[key] = trimmedValue\n\t}\n\tif cfg.MaxIterations <= 0 {\n\t\tcfg.MaxIterations = 32\n\t}\n\tif !isSupportedProviderType(cfg.Provider.Type) {\n\t\treturn errors.New(\"provider.type must be one of openai-compatible, openai, anthropic (or leave it empty with provider.auto_detect_type=true)\")\n\t}\n\tswitch cfg.ApprovalPolicy {\n\tcase \"\", \"on-request\":\n\t\tcfg.ApprovalPolicy = \"on-request\"\n\tcase \"always\", \"never\":\n\tdefault:\n\t\treturn errors.New(\"approval_policy must be one of always, on-request, never\")\n\t}\n\tif cfg.TokenQuota < 1 {\n\t\tcfg.TokenQuota = 5000\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StorageType) == \"\" {\n\t\tcfg.TokenUsage.StorageType = \"file\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.StoragePath) == \"\" {\n\t\tcfg.TokenUsage.StoragePath = \".bytemind/token_usage.json\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.BackupInterval) == \"\" {\n\t\tcfg.TokenUsage.BackupInterval = \"1m\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.MonitorInterval) == \"\" {\n\t\tcfg.TokenUsage.MonitorInterval = \"30s\"\n\t}\n\tif strings.TrimSpace(cfg.TokenUsage.DatabaseDriver) == \"\" {\n\t\tcfg.TokenUsage.DatabaseDriver = \"sqlite3\"\n\t}\n\tif cfg.TokenUsage.MaxSessions < 1 {\n\t\tcfg.TokenUsage.MaxSessions = 10000\n\t}\n\tif cfg.TokenUsage.RetentionDays < 1 {\n\t\tcfg.TokenUsage.RetentionDays = 30\n\t}\n\tif cfg.TokenUsage.AlertThreshold < 1 {\n\t\tcfg.TokenUsage.AlertThreshold = 1000000\n\t}\n\tif cfg.ContextBudget.WarningRatio <= 0 {\n\t\treturn errors.New(\"context_budget.warning_ratio must be > 0\")\n\t}\n\tif cfg.ContextBudget.CriticalRatio <= 0 || cfg.ContextBudget.CriticalRatio > 1 {\n\t\treturn errors.New(\"context_budget.critical_ratio must be > 0 and <= 1\")\n\t}\n\tif cfg.ContextBudget.WarningRatio >= cfg.ContextBudget.CriticalRatio {\n\t\treturn errors.New(\"context_budget.warning_ratio must be < context_budget.critical_ratio\")\n\t}\n\tif cfg.ContextBudget.MaxReactiveRetry < 0 {\n\t\treturn errors.New(\"context_budget.max_reactive_retry must be >= 0\")\n\t}\n\treturn nil\n}\n\nfunc normalizeProviderType(value string) string {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"\":\n\t\treturn \"\"\n\tcase \"openai-compatible\", \"openai_compatible\":\n\t\treturn \"openai-compatible\"\n\tcase \"openai\":\n\t\treturn \"openai\"\n\tcase \"anthropic\":\n\t\treturn \"anthropic\"\n\tdefault:\n\t\treturn strings.ToLower(strings.TrimSpace(value))\n\t}\n}\n\nfunc detectProviderType(cfg ProviderConfig) string {\n\tbaseURL := strings.ToLower(strings.TrimSpace(cfg.BaseURL))\n\tapiPath := strings.ToLower(strings.TrimSpace(cfg.APIPath))\n\tauthHeader := strings.ToLower(strings.TrimSpace(cfg.AuthHeader))\n\n\tif strings.Contains(apiPath, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(apiPath, \"/\"), \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif strings.Contains(baseURL, \"/v1/messages\") || strings.HasSuffix(strings.TrimRight(baseURL, \"/\"), \"/messages\") {\n\t\treturn \"anthropic\"\n\t}\n\tif hasHeaderName(cfg.ExtraHeaders, \"anthropic-version\") {\n\t\treturn \"anthropic\"\n\t}\n\tif authHeader == \"x-api-key\" && strings.Contains(apiPath, \"messages\") {\n\t\treturn \"anthropic\"\n\t}\n\treturn \"openai-compatible\"\n}\n\nfunc hasHeaderName(headers map[string]string, target string) bool {\n\tfor key := range headers {\n\t\tif strings.EqualFold(strings.TrimSpace(key), target) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSupportedProviderType(value string) bool {\n\tswitch value {\n\tcase \"openai-compatible\", \"openai\", \"anthropic\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc defaultBaseURL(providerType string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc defaultModel(providerType, baseURL string) string {\n\tswitch normalizeProviderType(providerType) {\n\tcase \"openai-compatible\", \"openai\", \"\":\n\t\tif strings.Contains(strings.ToLower(strings.TrimSpace(baseURL)), \"deepseek.com\") {\n\t\t\treturn \"deepseek-chat\"\n\t\t}\n\t\treturn defaultModelID\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n"}}, "tui/component_startup_guide.go": {"f259496ed7f959b3400c57e2d3d4d0a548d4b6a3": {"t": 1785746059.049636, "n": 12398, "h": "9b1a4108bbdf0aa6", "c": "package tui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/1024XEngineer/bytemind/internal/config\"\n\t\"github.com/1024XEngineer/bytemind/internal/llm\"\n\t\"github.com/1024XEngineer/bytemind/internal/provider\"\n)\n\nfunc (m *model) handleStartupGuideSubmission(rawInput string) error {\n\trawInput = strings.TrimSpace(rawInput)\n\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tif explicitField, explicitValue, ok := parseStartupConfigInput(rawInput); ok {\n\t\tfield = explicitField\n\t\trawInput = explicitValue\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tvalue, err := m.resolveStartupFieldValue(field, rawInput)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := m.applyStartupConfigField(field, value); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnext := startupNextField(field)\n\t\tif next == \"\" {\n\t\t\tnext = startupFieldAPIKey\n\t\t}\n\t\tm.setStartupGuideStep(next, \"\")\n\t\tm.input.Reset()\n\t\tm.clearPasteTransaction()\n\t\tm.clearVirtualPasteParts()\n\t\treturn nil\n\tcase startupFieldAPIKey:\n\t\treturn m.verifyAndFinalizeStartupAPIKey(rawInput)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n}\n\nfunc (m *model) verifyAndFinalizeStartupAPIKey(rawInput string) error {\n\tapiKey := sanitizeAPIKeyInput(rawInput)\n\tif apiKey == \"\" {\n\t\tapiKey = strings.TrimSpace(m.cfg.Provider.ResolveAPIKey())\n\t}\n\tif apiKey == \"\" {\n\t\treturn fmt.Errorf(\"please paste a non-empty API key\")\n\t}\n\n\tcheckCfg := m.cfg.Provider\n\tcheckCfg.APIKey = apiKey\n\tcheck := provider.CheckAvailability(context.Background(), checkCfg)\n\tif !check.Ready {\n\t\tm.llmConnected = false\n\t\tm.phase = \"error\"\n\t\trecoveryField := startupGuideRecoveryField(check)\n\t\tif recoveryField != startupFieldAPIKey {\n\t\t\tm.cfg.Provider.APIKey = apiKey\n\t\t\tm.input.Reset()\n\t\t\tm.clearPasteTransaction()\n\t\t\tm.releasePasteSubmitSuppression()\n\t\t}\n\t\tm.setStartupGuideStep(recoveryField, startupGuideIssueHint(check))\n\t\treturn nil\n\t}\n\n\twrittenPath, saveErr := config.UpsertProviderAPIKey(m.startupGuide.ConfigPath, apiKey)\n\n\tif envName := strings.TrimSpace(checkCfg.APIKeyEnv); envName != \"\" {\n\t\tif err := os.Setenv(envName, apiKey); err != nil {\n\t\t\twarnSetenv(envName, err)\n\t\t}\n\t} else {\n\t\tif err := os.Setenv(\"BYTEMIND_API_KEY\", apiKey); err != nil {\n\t\t\twarnSetenv(\"BYTEMIND_API_KEY\", err)\n\t\t}\n\t}\n\n\truntimeCfg := config.SyncProviderRuntimeWithProvider(m.cfg.ProviderRuntime, checkCfg)\n\tclient, err := provider.NewClientFromRuntime(runtimeCfg, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif m.runner != nil {\n\t\tif runtimeUpdater, ok := any(m.runner).(interface {\n\t\t\tUpdateProviderRuntime(config.ProviderRuntimeConfig, config.ProviderConfig, llm.Client)\n\t\t}); ok {\n\t\t\truntimeUpdater.UpdateProviderRuntime(runtimeCfg, checkCfg, client)\n\t\t} else {\n\t\t\tm.runner.UpdateProvider(checkCfg, client)\n\t\t}\n\t}\n\tm.cfg.Provider = checkCfg\n\tm.cfg.ProviderRuntime = runtimeCfg\n\tm.discoveredModels = nil\n\tm.refreshTokenBudget()\n\tm.syncTokenUsageComponent()\n\tm.startupGuide.Active = false\n\tm.statusNote = \"Provider configured and verified. You can start chatting.\"\n\tm.llmConnected = true\n\tm.phase = \"idle\"\n\tif saveErr != nil {\n\t\tm.statusNote = \"Provider verified, but config save failed: \" + compact(saveErr.Error(), 80)\n\t} else if strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.statusNote = \"Provider configured and verified. Saved to \" + compact(writtenPath, 48)\n\t}\n\tm.syncInputStyle()\n\tm.input.Reset()\n\tm.clearPasteTransaction()\n\tm.clearVirtualPasteParts()\n\treturn nil\n}\n\nfunc (m *model) applyStartupConfigField(field, value string) error {\n\tfield = strings.TrimSpace(field)\n\tvalue = strings.TrimSpace(value)\n\tif value == \"\" {\n\t\treturn fmt.Errorf(\"%s cannot be empty\", field)\n\t}\n\tpersistValue := value\n\n\tswitch field {\n\tcase \"model\":\n\t\tm.cfg.Provider.Model = value\n\tcase \"base_url\":\n\t\tm.cfg.Provider.BaseURL = value\n\tcase \"type\":\n\t\tnormalized, ok := normalizeStartupProviderType(value)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"provider must be openai-compatible or anthropic\")\n\t\t}\n\t\tm.cfg.Provider.Type = normalized\n\t\tpersistValue = normalized\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n\tm.cfg.ProviderRuntime = config.SyncProviderRuntimeWithProvider(m.cfg.ProviderRuntime, m.cfg.Provider)\n\tm.discoveredModels = nil\n\n\twrittenPath, err := config.UpsertProviderField(m.startupGuide.ConfigPath, field, persistValue)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.startupGuide.ConfigPath = writtenPath\n\t}\n\treturn nil\n}\n\nfunc parseStartupConfigInput(raw string) (field, value string, ok bool) {\n\ttrimmed := strings.TrimSpace(raw)\n\tlower := strings.ToLower(trimmed)\n\tif lower == \"\" {\n\t\treturn \"\", \"\", false\n\t}\n\n\tparse := func(alias, normalized string) (string, string, bool) {\n\t\tfor _, sep := range []string{\"=\", \":\"} {\n\t\t\tprefix := alias + sep\n\t\t\tif strings.HasPrefix(lower, prefix) {\n\t\t\t\tval := strings.TrimSpace(trimmed[len(prefix):])\n\t\t\t\treturn normalized, val, true\n\t\t\t}\n\t\t}\n\t\treturn \"\", \"\", false\n\t}\n\n\tfor _, candidate := range []struct {\n\t\talias string\n\t\tnormalized string\n\t}{\n\t\t{alias: \"model\", normalized: \"model\"},\n\t\t{alias: \"base_url\", normalized: \"base_url\"},\n\t\t{alias: \"baseurl\", normalized: \"base_url\"},\n\t\t{alias: \"base-url\", normalized: \"base_url\"},\n\t\t{alias: \"provider\", normalized: \"type\"},\n\t\t{alias: \"type\", normalized: \"type\"},\n\t\t{alias: \"provider_type\", normalized: \"type\"},\n\t\t{alias: \"api_key\", normalized: \"api_key\"},\n\t\t{alias: \"apikey\", normalized: \"api_key\"},\n\t\t{alias: \"key\", normalized: \"api_key\"},\n\t} {\n\t\tif field, value, ok := parse(candidate.alias, candidate.normalized); ok {\n\t\t\treturn field, value, true\n\t\t}\n\t}\n\n\treturn \"\", \"\", false\n}\n\nfunc sanitizeAPIKeyInput(raw string) string {\n\tvalue := strings.TrimSpace(raw)\n\tvalue = strings.Trim(value, \"\\\"'\")\n\tlower := strings.ToLower(value)\n\tif strings.HasPrefix(lower, \"authorization: bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"authorization: bearer \"):])\n\t}\n\tif strings.HasPrefix(lower, \"bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"bearer \"):])\n\t}\n\treturn strings.TrimSpace(value)\n}\n\nfunc normalizeStartupProviderType(value string) (string, bool) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"openai-compatible\", \"openai_compatible\", \"openai\":\n\t\treturn \"openai-compatible\", true\n\tcase \"anthropic\":\n\t\treturn \"anthropic\", true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\nfunc isStartupGuideField(field string) bool {\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel, startupFieldAPIKey:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc startupNextField(current string) string {\n\tfor i, field := range startupFieldOrder {\n\t\tif field == current {\n\t\t\tif i+1 >= len(startupFieldOrder) {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn startupFieldOrder[i+1]\n\t\t}\n\t}\n\treturn startupFieldType\n}\n\nfunc startupFieldStep(field string) (int, int) {\n\tfor i, item := range startupFieldOrder {\n\t\tif item == field {\n\t\t\treturn i + 1, len(startupFieldOrder)\n\t\t}\n\t}\n\treturn 1, len(startupFieldOrder)\n}\n\nfunc startupFieldName(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"provider\"\n\tcase startupFieldBaseURL:\n\t\treturn \"base_url\"\n\tcase startupFieldModel:\n\t\treturn \"model\"\n\tcase startupFieldAPIKey:\n\t\treturn \"api_key\"\n\tdefault:\n\t\treturn field\n\t}\n}\n\nfunc startupProviderDefaultBaseURL(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc startupProviderDefaultModel(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"GPT-5.4\"\n\t}\n}\n\nfunc (m model) startupCurrentValue(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Type)\n\tcase startupFieldBaseURL:\n\t\treturn strings.TrimSpace(m.cfg.Provider.BaseURL)\n\tcase startupFieldModel:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Model)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (m *model) resolveStartupFieldValue(field, rawInput string) (string, error) {\n\tvalue := strings.TrimSpace(rawInput)\n\tif value != \"\" {\n\t\treturn value, nil\n\t}\n\n\tcurrent := m.startupCurrentValue(field)\n\tif current != \"\" {\n\t\treturn current, nil\n\t}\n\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"openai-compatible\", nil\n\tcase startupFieldBaseURL:\n\t\treturn startupProviderDefaultBaseURL(m.cfg.Provider.Type), nil\n\tcase startupFieldModel:\n\t\tif fallback := startupProviderDefaultModel(m.cfg.Provider.Type); fallback != \"\" {\n\t\t\treturn fallback, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"please enter model name for provider %s\", strings.TrimSpace(m.cfg.Provider.Type))\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"%s cannot be empty\", startupFieldName(field))\n\t}\n}\n\nfunc (m *model) initializeStartupGuide() {\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tm.setStartupGuideStep(field, \"\")\n}\n\nfunc (m *model) setStartupGuideStep(field, issue string) {\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tstep, total := startupFieldStep(field)\n\tfieldName := startupFieldName(field)\n\tif strings.TrimSpace(issue) == \"\" {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s.\", step, total, fieldName)\n\t} else {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s. %s\", step, total, fieldName, issue)\n\t}\n\tm.statusNote = m.startupGuide.Status\n\tm.startupGuide.CurrentField = field\n\tm.startupGuide.Lines = startupGuideStepLines(field, m.cfg, m.startupGuide.ConfigPath, issue)\n\tm.syncInputStyle()\n}\n\nfunc startupGuideStepLines(field string, cfg config.Config, configPath, issue string) []string {\n\tlines := make([]string, 0, 8)\n\tswitch field {\n\tcase startupFieldType:\n\t\tlines = append(lines, \"Enter provider: openai-compatible or anthropic.\")\n\tcase startupFieldBaseURL:\n\t\tlines = append(lines, \"Enter provider base_url.\")\n\t\tlines = append(lines, \"Example: https://api.deepseek.com/v1\")\n\tcase startupFieldModel:\n\t\tlines = append(lines, \"Enter model name.\")\n\t\tlines = append(lines, \"Example: deepseek-chat or GPT-5.4\")\n\tcase startupFieldAPIKey:\n\t\tif strings.TrimSpace(cfg.Provider.ResolveAPIKey()) != \"\" {\n\t\t\tlines = append(lines, \"Press Enter to retry the current API key, or paste a new one.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Paste API key and press Enter.\")\n\t\t}\n\t\tlines = append(lines, \"Bytemind will verify it automatically.\")\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tcurrent := \"\"\n\t\tswitch field {\n\t\tcase startupFieldType:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Type)\n\t\tcase startupFieldBaseURL:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.BaseURL)\n\t\tcase startupFieldModel:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Model)\n\t\t}\n\t\tif current == \"\" {\n\t\t\tlines = append(lines, \"Press Enter to use default.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Press Enter to keep current: \"+current)\n\t\t}\n\t}\n\tif strings.TrimSpace(issue) != \"\" {\n\t\tlines = append(lines, \"Issue: \"+issue)\n\t}\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tlines = append(lines, \"Config file: \"+configPath)\n\t}\n\treturn lines\n}\n\nfunc startupGuideRecoveryField(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tdetail := strings.ToLower(strings.TrimSpace(check.Detail))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"), strings.Contains(reason, \"unauthorized\"):\n\t\treturn startupFieldAPIKey\n\tcase strings.Contains(reason, \"incomplete\"),\n\t\tstrings.Contains(reason, \"failed to build\"),\n\t\tstrings.Contains(reason, \"failed to reach\"),\n\t\tstrings.Contains(reason, \"not found\"),\n\t\tstrings.Contains(detail, \"base_url\"):\n\t\treturn startupFieldBaseURL\n\tdefault:\n\t\treturn startupFieldAPIKey\n\t}\n}\n\nfunc startupGuideIssueHint(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"):\n\t\treturn \"No API key is configured yet.\"\n\tcase strings.Contains(reason, \"unauthorized\"):\n\t\treturn \"The API key was rejected by the provider.\"\n\tcase strings.Contains(reason, \"failed to reach\"):\n\t\treturn \"Cannot reach provider endpoint. Check proxy or network.\"\n\tcase strings.Contains(reason, \"not found\"):\n\t\treturn \"Provider endpoint path looks incorrect.\"\n\tdefault:\n\t\tif strings.TrimSpace(check.Reason) == \"\" {\n\t\t\treturn \"Provider check failed.\"\n\t\t}\n\t\treturn compact(strings.TrimSpace(check.Reason), 90)\n\t}\n}\n"}, "eaf53f3c5d7f0efe14da1224dc8c1cddb4cff3cb": {"t": 1785749485.4019012, "n": 12398, "h": "9b1a4108bbdf0aa6", "c": "package tui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/1024XEngineer/bytemind/internal/config\"\n\t\"github.com/1024XEngineer/bytemind/internal/llm\"\n\t\"github.com/1024XEngineer/bytemind/internal/provider\"\n)\n\nfunc (m *model) handleStartupGuideSubmission(rawInput string) error {\n\trawInput = strings.TrimSpace(rawInput)\n\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tif explicitField, explicitValue, ok := parseStartupConfigInput(rawInput); ok {\n\t\tfield = explicitField\n\t\trawInput = explicitValue\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tvalue, err := m.resolveStartupFieldValue(field, rawInput)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := m.applyStartupConfigField(field, value); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnext := startupNextField(field)\n\t\tif next == \"\" {\n\t\t\tnext = startupFieldAPIKey\n\t\t}\n\t\tm.setStartupGuideStep(next, \"\")\n\t\tm.input.Reset()\n\t\tm.clearPasteTransaction()\n\t\tm.clearVirtualPasteParts()\n\t\treturn nil\n\tcase startupFieldAPIKey:\n\t\treturn m.verifyAndFinalizeStartupAPIKey(rawInput)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n}\n\nfunc (m *model) verifyAndFinalizeStartupAPIKey(rawInput string) error {\n\tapiKey := sanitizeAPIKeyInput(rawInput)\n\tif apiKey == \"\" {\n\t\tapiKey = strings.TrimSpace(m.cfg.Provider.ResolveAPIKey())\n\t}\n\tif apiKey == \"\" {\n\t\treturn fmt.Errorf(\"please paste a non-empty API key\")\n\t}\n\n\tcheckCfg := m.cfg.Provider\n\tcheckCfg.APIKey = apiKey\n\tcheck := provider.CheckAvailability(context.Background(), checkCfg)\n\tif !check.Ready {\n\t\tm.llmConnected = false\n\t\tm.phase = \"error\"\n\t\trecoveryField := startupGuideRecoveryField(check)\n\t\tif recoveryField != startupFieldAPIKey {\n\t\t\tm.cfg.Provider.APIKey = apiKey\n\t\t\tm.input.Reset()\n\t\t\tm.clearPasteTransaction()\n\t\t\tm.releasePasteSubmitSuppression()\n\t\t}\n\t\tm.setStartupGuideStep(recoveryField, startupGuideIssueHint(check))\n\t\treturn nil\n\t}\n\n\twrittenPath, saveErr := config.UpsertProviderAPIKey(m.startupGuide.ConfigPath, apiKey)\n\n\tif envName := strings.TrimSpace(checkCfg.APIKeyEnv); envName != \"\" {\n\t\tif err := os.Setenv(envName, apiKey); err != nil {\n\t\t\twarnSetenv(envName, err)\n\t\t}\n\t} else {\n\t\tif err := os.Setenv(\"BYTEMIND_API_KEY\", apiKey); err != nil {\n\t\t\twarnSetenv(\"BYTEMIND_API_KEY\", err)\n\t\t}\n\t}\n\n\truntimeCfg := config.SyncProviderRuntimeWithProvider(m.cfg.ProviderRuntime, checkCfg)\n\tclient, err := provider.NewClientFromRuntime(runtimeCfg, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif m.runner != nil {\n\t\tif runtimeUpdater, ok := any(m.runner).(interface {\n\t\t\tUpdateProviderRuntime(config.ProviderRuntimeConfig, config.ProviderConfig, llm.Client)\n\t\t}); ok {\n\t\t\truntimeUpdater.UpdateProviderRuntime(runtimeCfg, checkCfg, client)\n\t\t} else {\n\t\t\tm.runner.UpdateProvider(checkCfg, client)\n\t\t}\n\t}\n\tm.cfg.Provider = checkCfg\n\tm.cfg.ProviderRuntime = runtimeCfg\n\tm.discoveredModels = nil\n\tm.refreshTokenBudget()\n\tm.syncTokenUsageComponent()\n\tm.startupGuide.Active = false\n\tm.statusNote = \"Provider configured and verified. You can start chatting.\"\n\tm.llmConnected = true\n\tm.phase = \"idle\"\n\tif saveErr != nil {\n\t\tm.statusNote = \"Provider verified, but config save failed: \" + compact(saveErr.Error(), 80)\n\t} else if strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.statusNote = \"Provider configured and verified. Saved to \" + compact(writtenPath, 48)\n\t}\n\tm.syncInputStyle()\n\tm.input.Reset()\n\tm.clearPasteTransaction()\n\tm.clearVirtualPasteParts()\n\treturn nil\n}\n\nfunc (m *model) applyStartupConfigField(field, value string) error {\n\tfield = strings.TrimSpace(field)\n\tvalue = strings.TrimSpace(value)\n\tif value == \"\" {\n\t\treturn fmt.Errorf(\"%s cannot be empty\", field)\n\t}\n\tpersistValue := value\n\n\tswitch field {\n\tcase \"model\":\n\t\tm.cfg.Provider.Model = value\n\tcase \"base_url\":\n\t\tm.cfg.Provider.BaseURL = value\n\tcase \"type\":\n\t\tnormalized, ok := normalizeStartupProviderType(value)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"provider must be openai-compatible or anthropic\")\n\t\t}\n\t\tm.cfg.Provider.Type = normalized\n\t\tpersistValue = normalized\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n\tm.cfg.ProviderRuntime = config.SyncProviderRuntimeWithProvider(m.cfg.ProviderRuntime, m.cfg.Provider)\n\tm.discoveredModels = nil\n\n\twrittenPath, err := config.UpsertProviderField(m.startupGuide.ConfigPath, field, persistValue)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.startupGuide.ConfigPath = writtenPath\n\t}\n\treturn nil\n}\n\nfunc parseStartupConfigInput(raw string) (field, value string, ok bool) {\n\ttrimmed := strings.TrimSpace(raw)\n\tlower := strings.ToLower(trimmed)\n\tif lower == \"\" {\n\t\treturn \"\", \"\", false\n\t}\n\n\tparse := func(alias, normalized string) (string, string, bool) {\n\t\tfor _, sep := range []string{\"=\", \":\"} {\n\t\t\tprefix := alias + sep\n\t\t\tif strings.HasPrefix(lower, prefix) {\n\t\t\t\tval := strings.TrimSpace(trimmed[len(prefix):])\n\t\t\t\treturn normalized, val, true\n\t\t\t}\n\t\t}\n\t\treturn \"\", \"\", false\n\t}\n\n\tfor _, candidate := range []struct {\n\t\talias string\n\t\tnormalized string\n\t}{\n\t\t{alias: \"model\", normalized: \"model\"},\n\t\t{alias: \"base_url\", normalized: \"base_url\"},\n\t\t{alias: \"baseurl\", normalized: \"base_url\"},\n\t\t{alias: \"base-url\", normalized: \"base_url\"},\n\t\t{alias: \"provider\", normalized: \"type\"},\n\t\t{alias: \"type\", normalized: \"type\"},\n\t\t{alias: \"provider_type\", normalized: \"type\"},\n\t\t{alias: \"api_key\", normalized: \"api_key\"},\n\t\t{alias: \"apikey\", normalized: \"api_key\"},\n\t\t{alias: \"key\", normalized: \"api_key\"},\n\t} {\n\t\tif field, value, ok := parse(candidate.alias, candidate.normalized); ok {\n\t\t\treturn field, value, true\n\t\t}\n\t}\n\n\treturn \"\", \"\", false\n}\n\nfunc sanitizeAPIKeyInput(raw string) string {\n\tvalue := strings.TrimSpace(raw)\n\tvalue = strings.Trim(value, \"\\\"'\")\n\tlower := strings.ToLower(value)\n\tif strings.HasPrefix(lower, \"authorization: bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"authorization: bearer \"):])\n\t}\n\tif strings.HasPrefix(lower, \"bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"bearer \"):])\n\t}\n\treturn strings.TrimSpace(value)\n}\n\nfunc normalizeStartupProviderType(value string) (string, bool) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"openai-compatible\", \"openai_compatible\", \"openai\":\n\t\treturn \"openai-compatible\", true\n\tcase \"anthropic\":\n\t\treturn \"anthropic\", true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\nfunc isStartupGuideField(field string) bool {\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel, startupFieldAPIKey:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc startupNextField(current string) string {\n\tfor i, field := range startupFieldOrder {\n\t\tif field == current {\n\t\t\tif i+1 >= len(startupFieldOrder) {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn startupFieldOrder[i+1]\n\t\t}\n\t}\n\treturn startupFieldType\n}\n\nfunc startupFieldStep(field string) (int, int) {\n\tfor i, item := range startupFieldOrder {\n\t\tif item == field {\n\t\t\treturn i + 1, len(startupFieldOrder)\n\t\t}\n\t}\n\treturn 1, len(startupFieldOrder)\n}\n\nfunc startupFieldName(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"provider\"\n\tcase startupFieldBaseURL:\n\t\treturn \"base_url\"\n\tcase startupFieldModel:\n\t\treturn \"model\"\n\tcase startupFieldAPIKey:\n\t\treturn \"api_key\"\n\tdefault:\n\t\treturn field\n\t}\n}\n\nfunc startupProviderDefaultBaseURL(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc startupProviderDefaultModel(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"GPT-5.4\"\n\t}\n}\n\nfunc (m model) startupCurrentValue(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Type)\n\tcase startupFieldBaseURL:\n\t\treturn strings.TrimSpace(m.cfg.Provider.BaseURL)\n\tcase startupFieldModel:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Model)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (m *model) resolveStartupFieldValue(field, rawInput string) (string, error) {\n\tvalue := strings.TrimSpace(rawInput)\n\tif value != \"\" {\n\t\treturn value, nil\n\t}\n\n\tcurrent := m.startupCurrentValue(field)\n\tif current != \"\" {\n\t\treturn current, nil\n\t}\n\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"openai-compatible\", nil\n\tcase startupFieldBaseURL:\n\t\treturn startupProviderDefaultBaseURL(m.cfg.Provider.Type), nil\n\tcase startupFieldModel:\n\t\tif fallback := startupProviderDefaultModel(m.cfg.Provider.Type); fallback != \"\" {\n\t\t\treturn fallback, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"please enter model name for provider %s\", strings.TrimSpace(m.cfg.Provider.Type))\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"%s cannot be empty\", startupFieldName(field))\n\t}\n}\n\nfunc (m *model) initializeStartupGuide() {\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tm.setStartupGuideStep(field, \"\")\n}\n\nfunc (m *model) setStartupGuideStep(field, issue string) {\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tstep, total := startupFieldStep(field)\n\tfieldName := startupFieldName(field)\n\tif strings.TrimSpace(issue) == \"\" {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s.\", step, total, fieldName)\n\t} else {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s. %s\", step, total, fieldName, issue)\n\t}\n\tm.statusNote = m.startupGuide.Status\n\tm.startupGuide.CurrentField = field\n\tm.startupGuide.Lines = startupGuideStepLines(field, m.cfg, m.startupGuide.ConfigPath, issue)\n\tm.syncInputStyle()\n}\n\nfunc startupGuideStepLines(field string, cfg config.Config, configPath, issue string) []string {\n\tlines := make([]string, 0, 8)\n\tswitch field {\n\tcase startupFieldType:\n\t\tlines = append(lines, \"Enter provider: openai-compatible or anthropic.\")\n\tcase startupFieldBaseURL:\n\t\tlines = append(lines, \"Enter provider base_url.\")\n\t\tlines = append(lines, \"Example: https://api.deepseek.com/v1\")\n\tcase startupFieldModel:\n\t\tlines = append(lines, \"Enter model name.\")\n\t\tlines = append(lines, \"Example: deepseek-chat or GPT-5.4\")\n\tcase startupFieldAPIKey:\n\t\tif strings.TrimSpace(cfg.Provider.ResolveAPIKey()) != \"\" {\n\t\t\tlines = append(lines, \"Press Enter to retry the current API key, or paste a new one.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Paste API key and press Enter.\")\n\t\t}\n\t\tlines = append(lines, \"Bytemind will verify it automatically.\")\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tcurrent := \"\"\n\t\tswitch field {\n\t\tcase startupFieldType:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Type)\n\t\tcase startupFieldBaseURL:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.BaseURL)\n\t\tcase startupFieldModel:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Model)\n\t\t}\n\t\tif current == \"\" {\n\t\t\tlines = append(lines, \"Press Enter to use default.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Press Enter to keep current: \"+current)\n\t\t}\n\t}\n\tif strings.TrimSpace(issue) != \"\" {\n\t\tlines = append(lines, \"Issue: \"+issue)\n\t}\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tlines = append(lines, \"Config file: \"+configPath)\n\t}\n\treturn lines\n}\n\nfunc startupGuideRecoveryField(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tdetail := strings.ToLower(strings.TrimSpace(check.Detail))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"), strings.Contains(reason, \"unauthorized\"):\n\t\treturn startupFieldAPIKey\n\tcase strings.Contains(reason, \"incomplete\"),\n\t\tstrings.Contains(reason, \"failed to build\"),\n\t\tstrings.Contains(reason, \"failed to reach\"),\n\t\tstrings.Contains(reason, \"not found\"),\n\t\tstrings.Contains(detail, \"base_url\"):\n\t\treturn startupFieldBaseURL\n\tdefault:\n\t\treturn startupFieldAPIKey\n\t}\n}\n\nfunc startupGuideIssueHint(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"):\n\t\treturn \"No API key is configured yet.\"\n\tcase strings.Contains(reason, \"unauthorized\"):\n\t\treturn \"The API key was rejected by the provider.\"\n\tcase strings.Contains(reason, \"failed to reach\"):\n\t\treturn \"Cannot reach provider endpoint. Check proxy or network.\"\n\tcase strings.Contains(reason, \"not found\"):\n\t\treturn \"Provider endpoint path looks incorrect.\"\n\tdefault:\n\t\tif strings.TrimSpace(check.Reason) == \"\" {\n\t\t\treturn \"Provider check failed.\"\n\t\t}\n\t\treturn compact(strings.TrimSpace(check.Reason), 90)\n\t}\n}\n"}, "71c184b5e0c671e342e86230c84b16dfc8774863": {"t": 1785749486.5010219, "n": 12063, "h": "5ace0ed06c805603", "c": "package tui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/1024XEngineer/bytemind/internal/config\"\n\t\"github.com/1024XEngineer/bytemind/internal/provider\"\n)\n\nfunc (m *model) handleStartupGuideSubmission(rawInput string) error {\n\trawInput = strings.TrimSpace(rawInput)\n\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tif explicitField, explicitValue, ok := parseStartupConfigInput(rawInput); ok {\n\t\tfield = explicitField\n\t\trawInput = explicitValue\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tvalue, err := m.resolveStartupFieldValue(field, rawInput)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := m.applyStartupConfigField(field, value); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnext := startupNextField(field)\n\t\tif next == \"\" {\n\t\t\tnext = startupFieldAPIKey\n\t\t}\n\t\tm.setStartupGuideStep(next, \"\")\n\t\tm.input.Reset()\n\t\tm.clearPasteTransaction()\n\t\tm.clearVirtualPasteParts()\n\t\treturn nil\n\tcase startupFieldAPIKey:\n\t\treturn m.verifyAndFinalizeStartupAPIKey(rawInput)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n}\n\nfunc (m *model) verifyAndFinalizeStartupAPIKey(rawInput string) error {\n\tapiKey := sanitizeAPIKeyInput(rawInput)\n\tif apiKey == \"\" {\n\t\tapiKey = strings.TrimSpace(m.cfg.Provider.ResolveAPIKey())\n\t}\n\tif apiKey == \"\" {\n\t\treturn fmt.Errorf(\"please paste a non-empty API key\")\n\t}\n\n\tcheckCfg := m.cfg.Provider\n\tcheckCfg.APIKey = apiKey\n\tcheck := provider.CheckAvailability(context.Background(), checkCfg)\n\tif !check.Ready {\n\t\tm.llmConnected = false\n\t\tm.phase = \"error\"\n\t\trecoveryField := startupGuideRecoveryField(check)\n\t\tif recoveryField != startupFieldAPIKey {\n\t\t\tm.cfg.Provider.APIKey = apiKey\n\t\t\tm.input.Reset()\n\t\t\tm.clearPasteTransaction()\n\t\t\tm.releasePasteSubmitSuppression()\n\t\t}\n\t\tm.setStartupGuideStep(recoveryField, startupGuideIssueHint(check))\n\t\treturn nil\n\t}\n\n\twrittenPath, saveErr := config.UpsertProviderAPIKey(m.startupGuide.ConfigPath, apiKey)\n\n\tif envName := strings.TrimSpace(checkCfg.APIKeyEnv); envName != \"\" {\n\t\tif err := os.Setenv(envName, apiKey); err != nil {\n\t\t\twarnSetenv(envName, err)\n\t\t}\n\t} else {\n\t\tif err := os.Setenv(\"BYTEMIND_API_KEY\", apiKey); err != nil {\n\t\t\twarnSetenv(\"BYTEMIND_API_KEY\", err)\n\t\t}\n\t}\n\n\tclient, err := provider.NewClient(checkCfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif m.runner != nil {\n\t\tm.runner.UpdateProvider(checkCfg, client)\n\t}\n\tm.cfg.Provider = checkCfg\n\tm.cfg.ProviderRuntime = config.SyncProviderRuntimeWithProvider(m.cfg.ProviderRuntime, checkCfg)\n\tm.discoveredModels = nil\n\tm.refreshTokenBudget()\n\tm.syncTokenUsageComponent()\n\tm.startupGuide.Active = false\n\tm.statusNote = \"Provider configured and verified. You can start chatting.\"\n\tm.llmConnected = true\n\tm.phase = \"idle\"\n\tif saveErr != nil {\n\t\tm.statusNote = \"Provider verified, but config save failed: \" + compact(saveErr.Error(), 80)\n\t} else if strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.statusNote = \"Provider configured and verified. Saved to \" + compact(writtenPath, 48)\n\t}\n\tm.syncInputStyle()\n\tm.input.Reset()\n\tm.clearPasteTransaction()\n\tm.clearVirtualPasteParts()\n\treturn nil\n}\n\nfunc (m *model) applyStartupConfigField(field, value string) error {\n\tfield = strings.TrimSpace(field)\n\tvalue = strings.TrimSpace(value)\n\tif value == \"\" {\n\t\treturn fmt.Errorf(\"%s cannot be empty\", field)\n\t}\n\tpersistValue := value\n\n\tswitch field {\n\tcase \"model\":\n\t\tm.cfg.Provider.Model = value\n\tcase \"base_url\":\n\t\tm.cfg.Provider.BaseURL = value\n\tcase \"type\":\n\t\tnormalized, ok := normalizeStartupProviderType(value)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"provider must be openai-compatible or anthropic\")\n\t\t}\n\t\tm.cfg.Provider.Type = normalized\n\t\tpersistValue = normalized\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n\tm.cfg.ProviderRuntime = config.SyncProviderRuntimeWithProvider(m.cfg.ProviderRuntime, m.cfg.Provider)\n\tm.discoveredModels = nil\n\n\twrittenPath, err := config.UpsertProviderField(m.startupGuide.ConfigPath, field, persistValue)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.startupGuide.ConfigPath = writtenPath\n\t}\n\treturn nil\n}\n\nfunc parseStartupConfigInput(raw string) (field, value string, ok bool) {\n\ttrimmed := strings.TrimSpace(raw)\n\tlower := strings.ToLower(trimmed)\n\tif lower == \"\" {\n\t\treturn \"\", \"\", false\n\t}\n\n\tparse := func(alias, normalized string) (string, string, bool) {\n\t\tfor _, sep := range []string{\"=\", \":\"} {\n\t\t\tprefix := alias + sep\n\t\t\tif strings.HasPrefix(lower, prefix) {\n\t\t\t\tval := strings.TrimSpace(trimmed[len(prefix):])\n\t\t\t\treturn normalized, val, true\n\t\t\t}\n\t\t}\n\t\treturn \"\", \"\", false\n\t}\n\n\tfor _, candidate := range []struct {\n\t\talias string\n\t\tnormalized string\n\t}{\n\t\t{alias: \"model\", normalized: \"model\"},\n\t\t{alias: \"base_url\", normalized: \"base_url\"},\n\t\t{alias: \"baseurl\", normalized: \"base_url\"},\n\t\t{alias: \"base-url\", normalized: \"base_url\"},\n\t\t{alias: \"provider\", normalized: \"type\"},\n\t\t{alias: \"type\", normalized: \"type\"},\n\t\t{alias: \"provider_type\", normalized: \"type\"},\n\t\t{alias: \"api_key\", normalized: \"api_key\"},\n\t\t{alias: \"apikey\", normalized: \"api_key\"},\n\t\t{alias: \"key\", normalized: \"api_key\"},\n\t} {\n\t\tif field, value, ok := parse(candidate.alias, candidate.normalized); ok {\n\t\t\treturn field, value, true\n\t\t}\n\t}\n\n\treturn \"\", \"\", false\n}\n\nfunc sanitizeAPIKeyInput(raw string) string {\n\tvalue := strings.TrimSpace(raw)\n\tvalue = strings.Trim(value, \"\\\"'\")\n\tlower := strings.ToLower(value)\n\tif strings.HasPrefix(lower, \"authorization: bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"authorization: bearer \"):])\n\t}\n\tif strings.HasPrefix(lower, \"bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"bearer \"):])\n\t}\n\treturn strings.TrimSpace(value)\n}\n\nfunc normalizeStartupProviderType(value string) (string, bool) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"openai-compatible\", \"openai_compatible\", \"openai\":\n\t\treturn \"openai-compatible\", true\n\tcase \"anthropic\":\n\t\treturn \"anthropic\", true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\nfunc isStartupGuideField(field string) bool {\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel, startupFieldAPIKey:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc startupNextField(current string) string {\n\tfor i, field := range startupFieldOrder {\n\t\tif field == current {\n\t\t\tif i+1 >= len(startupFieldOrder) {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn startupFieldOrder[i+1]\n\t\t}\n\t}\n\treturn startupFieldType\n}\n\nfunc startupFieldStep(field string) (int, int) {\n\tfor i, item := range startupFieldOrder {\n\t\tif item == field {\n\t\t\treturn i + 1, len(startupFieldOrder)\n\t\t}\n\t}\n\treturn 1, len(startupFieldOrder)\n}\n\nfunc startupFieldName(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"provider\"\n\tcase startupFieldBaseURL:\n\t\treturn \"base_url\"\n\tcase startupFieldModel:\n\t\treturn \"model\"\n\tcase startupFieldAPIKey:\n\t\treturn \"api_key\"\n\tdefault:\n\t\treturn field\n\t}\n}\n\nfunc startupProviderDefaultBaseURL(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc startupProviderDefaultModel(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"GPT-5.4\"\n\t}\n}\n\nfunc (m model) startupCurrentValue(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Type)\n\tcase startupFieldBaseURL:\n\t\treturn strings.TrimSpace(m.cfg.Provider.BaseURL)\n\tcase startupFieldModel:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Model)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (m *model) resolveStartupFieldValue(field, rawInput string) (string, error) {\n\tvalue := strings.TrimSpace(rawInput)\n\tif value != \"\" {\n\t\treturn value, nil\n\t}\n\n\tcurrent := m.startupCurrentValue(field)\n\tif current != \"\" {\n\t\treturn current, nil\n\t}\n\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"openai-compatible\", nil\n\tcase startupFieldBaseURL:\n\t\treturn startupProviderDefaultBaseURL(m.cfg.Provider.Type), nil\n\tcase startupFieldModel:\n\t\tif fallback := startupProviderDefaultModel(m.cfg.Provider.Type); fallback != \"\" {\n\t\t\treturn fallback, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"please enter model name for provider %s\", strings.TrimSpace(m.cfg.Provider.Type))\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"%s cannot be empty\", startupFieldName(field))\n\t}\n}\n\nfunc (m *model) initializeStartupGuide() {\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tm.setStartupGuideStep(field, \"\")\n}\n\nfunc (m *model) setStartupGuideStep(field, issue string) {\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tstep, total := startupFieldStep(field)\n\tfieldName := startupFieldName(field)\n\tif strings.TrimSpace(issue) == \"\" {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s.\", step, total, fieldName)\n\t} else {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s. %s\", step, total, fieldName, issue)\n\t}\n\tm.statusNote = m.startupGuide.Status\n\tm.startupGuide.CurrentField = field\n\tm.startupGuide.Lines = startupGuideStepLines(field, m.cfg, m.startupGuide.ConfigPath, issue)\n\tm.syncInputStyle()\n}\n\nfunc startupGuideStepLines(field string, cfg config.Config, configPath, issue string) []string {\n\tlines := make([]string, 0, 8)\n\tswitch field {\n\tcase startupFieldType:\n\t\tlines = append(lines, \"Enter provider: openai-compatible or anthropic.\")\n\tcase startupFieldBaseURL:\n\t\tlines = append(lines, \"Enter provider base_url.\")\n\t\tlines = append(lines, \"Example: https://api.deepseek.com/v1\")\n\tcase startupFieldModel:\n\t\tlines = append(lines, \"Enter model name.\")\n\t\tlines = append(lines, \"Example: deepseek-chat or GPT-5.4\")\n\tcase startupFieldAPIKey:\n\t\tif strings.TrimSpace(cfg.Provider.ResolveAPIKey()) != \"\" {\n\t\t\tlines = append(lines, \"Press Enter to retry the current API key, or paste a new one.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Paste API key and press Enter.\")\n\t\t}\n\t\tlines = append(lines, \"Bytemind will verify it automatically.\")\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tcurrent := \"\"\n\t\tswitch field {\n\t\tcase startupFieldType:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Type)\n\t\tcase startupFieldBaseURL:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.BaseURL)\n\t\tcase startupFieldModel:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Model)\n\t\t}\n\t\tif current == \"\" {\n\t\t\tlines = append(lines, \"Press Enter to use default.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Press Enter to keep current: \"+current)\n\t\t}\n\t}\n\tif strings.TrimSpace(issue) != \"\" {\n\t\tlines = append(lines, \"Issue: \"+issue)\n\t}\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tlines = append(lines, \"Config file: \"+configPath)\n\t}\n\treturn lines\n}\n\nfunc startupGuideRecoveryField(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tdetail := strings.ToLower(strings.TrimSpace(check.Detail))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"), strings.Contains(reason, \"unauthorized\"):\n\t\treturn startupFieldAPIKey\n\tcase strings.Contains(reason, \"incomplete\"),\n\t\tstrings.Contains(reason, \"failed to build\"),\n\t\tstrings.Contains(reason, \"failed to reach\"),\n\t\tstrings.Contains(reason, \"not found\"),\n\t\tstrings.Contains(detail, \"base_url\"):\n\t\treturn startupFieldBaseURL\n\tdefault:\n\t\treturn startupFieldAPIKey\n\t}\n}\n\nfunc startupGuideIssueHint(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"):\n\t\treturn \"No API key is configured yet.\"\n\tcase strings.Contains(reason, \"unauthorized\"):\n\t\treturn \"The API key was rejected by the provider.\"\n\tcase strings.Contains(reason, \"failed to reach\"):\n\t\treturn \"Cannot reach provider endpoint. Check proxy or network.\"\n\tcase strings.Contains(reason, \"not found\"):\n\t\treturn \"Provider endpoint path looks incorrect.\"\n\tdefault:\n\t\tif strings.TrimSpace(check.Reason) == \"\" {\n\t\t\treturn \"Provider check failed.\"\n\t\t}\n\t\treturn compact(strings.TrimSpace(check.Reason), 90)\n\t}\n}\n"}, "1340fbb7c322cb6809e6ddb68c00c450ced6ced2": {"t": 1785749487.6146414, "n": 11758, "h": "539421e154ed4e6b", "c": "package tui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/1024XEngineer/bytemind/internal/config\"\n\t\"github.com/1024XEngineer/bytemind/internal/provider\"\n)\n\nfunc (m *model) handleStartupGuideSubmission(rawInput string) error {\n\trawInput = strings.TrimSpace(rawInput)\n\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tif explicitField, explicitValue, ok := parseStartupConfigInput(rawInput); ok {\n\t\tfield = explicitField\n\t\trawInput = explicitValue\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tvalue, err := m.resolveStartupFieldValue(field, rawInput)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := m.applyStartupConfigField(field, value); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnext := startupNextField(field)\n\t\tif next == \"\" {\n\t\t\tnext = startupFieldAPIKey\n\t\t}\n\t\tm.setStartupGuideStep(next, \"\")\n\t\tm.input.Reset()\n\t\tm.clearPasteTransaction()\n\t\tm.clearVirtualPasteParts()\n\t\treturn nil\n\tcase startupFieldAPIKey:\n\t\treturn m.verifyAndFinalizeStartupAPIKey(rawInput)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n}\n\nfunc (m *model) verifyAndFinalizeStartupAPIKey(rawInput string) error {\n\tapiKey := sanitizeAPIKeyInput(rawInput)\n\tif apiKey == \"\" {\n\t\tapiKey = strings.TrimSpace(m.cfg.Provider.ResolveAPIKey())\n\t}\n\tif apiKey == \"\" {\n\t\treturn fmt.Errorf(\"please paste a non-empty API key\")\n\t}\n\n\tcheckCfg := m.cfg.Provider\n\tcheckCfg.APIKey = apiKey\n\tcheck := provider.CheckAvailability(context.Background(), checkCfg)\n\tif !check.Ready {\n\t\tm.llmConnected = false\n\t\tm.phase = \"error\"\n\t\trecoveryField := startupGuideRecoveryField(check)\n\t\tif recoveryField != startupFieldAPIKey {\n\t\t\tm.cfg.Provider.APIKey = apiKey\n\t\t\tm.input.Reset()\n\t\t\tm.clearPasteTransaction()\n\t\t\tm.releasePasteSubmitSuppression()\n\t\t}\n\t\tm.setStartupGuideStep(recoveryField, startupGuideIssueHint(check))\n\t\treturn nil\n\t}\n\n\twrittenPath, saveErr := config.UpsertProviderAPIKey(m.startupGuide.ConfigPath, apiKey)\n\n\tif envName := strings.TrimSpace(checkCfg.APIKeyEnv); envName != \"\" {\n\t\tif err := os.Setenv(envName, apiKey); err != nil {\n\t\t\twarnSetenv(envName, err)\n\t\t}\n\t} else {\n\t\tif err := os.Setenv(\"BYTEMIND_API_KEY\", apiKey); err != nil {\n\t\t\twarnSetenv(\"BYTEMIND_API_KEY\", err)\n\t\t}\n\t}\n\n\tclient, err := provider.NewClient(checkCfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif m.runner != nil {\n\t\tm.runner.UpdateProvider(checkCfg, client)\n\t}\n\tm.cfg.Provider = checkCfg\n\tm.startupGuide.Active = false\n\tm.statusNote = \"Provider configured and verified. You can start chatting.\"\n\tm.llmConnected = true\n\tm.phase = \"idle\"\n\tif saveErr != nil {\n\t\tm.statusNote = \"Provider verified, but config save failed: \" + compact(saveErr.Error(), 80)\n\t} else if strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.statusNote = \"Provider configured and verified. Saved to \" + compact(writtenPath, 48)\n\t}\n\tm.syncInputStyle()\n\tm.input.Reset()\n\tm.clearPasteTransaction()\n\tm.clearVirtualPasteParts()\n\treturn nil\n}\n\nfunc (m *model) applyStartupConfigField(field, value string) error {\n\tfield = strings.TrimSpace(field)\n\tvalue = strings.TrimSpace(value)\n\tif value == \"\" {\n\t\treturn fmt.Errorf(\"%s cannot be empty\", field)\n\t}\n\tpersistValue := value\n\n\tswitch field {\n\tcase \"model\":\n\t\tm.cfg.Provider.Model = value\n\tcase \"base_url\":\n\t\tm.cfg.Provider.BaseURL = value\n\tcase \"type\":\n\t\tnormalized, ok := normalizeStartupProviderType(value)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"provider must be openai-compatible or anthropic\")\n\t\t}\n\t\tm.cfg.Provider.Type = normalized\n\t\tpersistValue = normalized\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n\n\twrittenPath, err := config.UpsertProviderField(m.startupGuide.ConfigPath, field, persistValue)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.startupGuide.ConfigPath = writtenPath\n\t}\n\treturn nil\n}\n\nfunc parseStartupConfigInput(raw string) (field, value string, ok bool) {\n\ttrimmed := strings.TrimSpace(raw)\n\tlower := strings.ToLower(trimmed)\n\tif lower == \"\" {\n\t\treturn \"\", \"\", false\n\t}\n\n\tparse := func(alias, normalized string) (string, string, bool) {\n\t\tfor _, sep := range []string{\"=\", \":\"} {\n\t\t\tprefix := alias + sep\n\t\t\tif strings.HasPrefix(lower, prefix) {\n\t\t\t\tval := strings.TrimSpace(trimmed[len(prefix):])\n\t\t\t\treturn normalized, val, true\n\t\t\t}\n\t\t}\n\t\treturn \"\", \"\", false\n\t}\n\n\tfor _, candidate := range []struct {\n\t\talias string\n\t\tnormalized string\n\t}{\n\t\t{alias: \"model\", normalized: \"model\"},\n\t\t{alias: \"base_url\", normalized: \"base_url\"},\n\t\t{alias: \"baseurl\", normalized: \"base_url\"},\n\t\t{alias: \"base-url\", normalized: \"base_url\"},\n\t\t{alias: \"provider\", normalized: \"type\"},\n\t\t{alias: \"type\", normalized: \"type\"},\n\t\t{alias: \"provider_type\", normalized: \"type\"},\n\t\t{alias: \"api_key\", normalized: \"api_key\"},\n\t\t{alias: \"apikey\", normalized: \"api_key\"},\n\t\t{alias: \"key\", normalized: \"api_key\"},\n\t} {\n\t\tif field, value, ok := parse(candidate.alias, candidate.normalized); ok {\n\t\t\treturn field, value, true\n\t\t}\n\t}\n\n\treturn \"\", \"\", false\n}\n\nfunc sanitizeAPIKeyInput(raw string) string {\n\tvalue := strings.TrimSpace(raw)\n\tvalue = strings.Trim(value, \"\\\"'\")\n\tlower := strings.ToLower(value)\n\tif strings.HasPrefix(lower, \"authorization: bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"authorization: bearer \"):])\n\t}\n\tif strings.HasPrefix(lower, \"bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"bearer \"):])\n\t}\n\treturn strings.TrimSpace(value)\n}\n\nfunc normalizeStartupProviderType(value string) (string, bool) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"openai-compatible\", \"openai_compatible\", \"openai\":\n\t\treturn \"openai-compatible\", true\n\tcase \"anthropic\":\n\t\treturn \"anthropic\", true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\nfunc isStartupGuideField(field string) bool {\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel, startupFieldAPIKey:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc startupNextField(current string) string {\n\tfor i, field := range startupFieldOrder {\n\t\tif field == current {\n\t\t\tif i+1 >= len(startupFieldOrder) {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn startupFieldOrder[i+1]\n\t\t}\n\t}\n\treturn startupFieldType\n}\n\nfunc startupFieldStep(field string) (int, int) {\n\tfor i, item := range startupFieldOrder {\n\t\tif item == field {\n\t\t\treturn i + 1, len(startupFieldOrder)\n\t\t}\n\t}\n\treturn 1, len(startupFieldOrder)\n}\n\nfunc startupFieldName(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"provider\"\n\tcase startupFieldBaseURL:\n\t\treturn \"base_url\"\n\tcase startupFieldModel:\n\t\treturn \"model\"\n\tcase startupFieldAPIKey:\n\t\treturn \"api_key\"\n\tdefault:\n\t\treturn field\n\t}\n}\n\nfunc startupProviderDefaultBaseURL(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc startupProviderDefaultModel(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"GPT-5.4\"\n\t}\n}\n\nfunc (m model) startupCurrentValue(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Type)\n\tcase startupFieldBaseURL:\n\t\treturn strings.TrimSpace(m.cfg.Provider.BaseURL)\n\tcase startupFieldModel:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Model)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (m *model) resolveStartupFieldValue(field, rawInput string) (string, error) {\n\tvalue := strings.TrimSpace(rawInput)\n\tif value != \"\" {\n\t\treturn value, nil\n\t}\n\n\tcurrent := m.startupCurrentValue(field)\n\tif current != \"\" {\n\t\treturn current, nil\n\t}\n\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"openai-compatible\", nil\n\tcase startupFieldBaseURL:\n\t\treturn startupProviderDefaultBaseURL(m.cfg.Provider.Type), nil\n\tcase startupFieldModel:\n\t\tif fallback := startupProviderDefaultModel(m.cfg.Provider.Type); fallback != \"\" {\n\t\t\treturn fallback, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"please enter model name for provider %s\", strings.TrimSpace(m.cfg.Provider.Type))\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"%s cannot be empty\", startupFieldName(field))\n\t}\n}\n\nfunc (m *model) initializeStartupGuide() {\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tm.setStartupGuideStep(field, \"\")\n}\n\nfunc (m *model) setStartupGuideStep(field, issue string) {\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tstep, total := startupFieldStep(field)\n\tfieldName := startupFieldName(field)\n\tif strings.TrimSpace(issue) == \"\" {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s.\", step, total, fieldName)\n\t} else {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s. %s\", step, total, fieldName, issue)\n\t}\n\tm.statusNote = m.startupGuide.Status\n\tm.startupGuide.CurrentField = field\n\tm.startupGuide.Lines = startupGuideStepLines(field, m.cfg, m.startupGuide.ConfigPath, issue)\n\tm.syncInputStyle()\n}\n\nfunc startupGuideStepLines(field string, cfg config.Config, configPath, issue string) []string {\n\tlines := make([]string, 0, 8)\n\tswitch field {\n\tcase startupFieldType:\n\t\tlines = append(lines, \"Enter provider: openai-compatible or anthropic.\")\n\tcase startupFieldBaseURL:\n\t\tlines = append(lines, \"Enter provider base_url.\")\n\t\tlines = append(lines, \"Example: https://api.deepseek.com/v1\")\n\tcase startupFieldModel:\n\t\tlines = append(lines, \"Enter model name.\")\n\t\tlines = append(lines, \"Example: deepseek-chat or GPT-5.4\")\n\tcase startupFieldAPIKey:\n\t\tif strings.TrimSpace(cfg.Provider.ResolveAPIKey()) != \"\" {\n\t\t\tlines = append(lines, \"Press Enter to retry the current API key, or paste a new one.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Paste API key and press Enter.\")\n\t\t}\n\t\tlines = append(lines, \"Bytemind will verify it automatically.\")\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tcurrent := \"\"\n\t\tswitch field {\n\t\tcase startupFieldType:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Type)\n\t\tcase startupFieldBaseURL:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.BaseURL)\n\t\tcase startupFieldModel:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Model)\n\t\t}\n\t\tif current == \"\" {\n\t\t\tlines = append(lines, \"Press Enter to use default.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Press Enter to keep current: \"+current)\n\t\t}\n\t}\n\tif strings.TrimSpace(issue) != \"\" {\n\t\tlines = append(lines, \"Issue: \"+issue)\n\t}\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tlines = append(lines, \"Config file: \"+configPath)\n\t}\n\treturn lines\n}\n\nfunc startupGuideRecoveryField(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tdetail := strings.ToLower(strings.TrimSpace(check.Detail))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"), strings.Contains(reason, \"unauthorized\"):\n\t\treturn startupFieldAPIKey\n\tcase strings.Contains(reason, \"incomplete\"),\n\t\tstrings.Contains(reason, \"failed to build\"),\n\t\tstrings.Contains(reason, \"failed to reach\"),\n\t\tstrings.Contains(reason, \"not found\"),\n\t\tstrings.Contains(detail, \"base_url\"):\n\t\treturn startupFieldBaseURL\n\tdefault:\n\t\treturn startupFieldAPIKey\n\t}\n}\n\nfunc startupGuideIssueHint(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"):\n\t\treturn \"No API key is configured yet.\"\n\tcase strings.Contains(reason, \"unauthorized\"):\n\t\treturn \"The API key was rejected by the provider.\"\n\tcase strings.Contains(reason, \"failed to reach\"):\n\t\treturn \"Cannot reach provider endpoint. Check proxy or network.\"\n\tcase strings.Contains(reason, \"not found\"):\n\t\treturn \"Provider endpoint path looks incorrect.\"\n\tdefault:\n\t\tif strings.TrimSpace(check.Reason) == \"\" {\n\t\t\treturn \"Provider check failed.\"\n\t\t}\n\t\treturn compact(strings.TrimSpace(check.Reason), 90)\n\t}\n}\n"}, "2e81216f4f6ce2b415bb7545f661a5bf9ff9c7d3": {"t": 1785749488.7260175, "n": 10678, "h": "6d6d1b4d383edf31", "c": "package tui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/1024XEngineer/bytemind/internal/config\"\n\t\"github.com/1024XEngineer/bytemind/internal/provider\"\n)\n\nfunc (m *model) handleStartupGuideSubmission(rawInput string) error {\n\trawInput = strings.TrimSpace(rawInput)\n\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tif explicitField, explicitValue, ok := parseStartupConfigInput(rawInput); ok {\n\t\tfield = explicitField\n\t\trawInput = explicitValue\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tvalue, err := m.resolveStartupFieldValue(field, rawInput)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := m.applyStartupConfigField(field, value); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnext := startupNextField(field)\n\t\tif next == \"\" {\n\t\t\tnext = startupFieldAPIKey\n\t\t}\n\t\tm.setStartupGuideStep(next, \"\")\n\t\tm.input.Reset()\n\t\tm.clearPasteTransaction()\n\t\tm.clearVirtualPasteParts()\n\t\treturn nil\n\tcase startupFieldAPIKey:\n\t\treturn m.verifyAndFinalizeStartupAPIKey(rawInput)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n}\n\nfunc (m *model) verifyAndFinalizeStartupAPIKey(rawInput string) error {\n\tapiKey := sanitizeAPIKeyInput(rawInput)\n\tif apiKey == \"\" {\n\t\treturn fmt.Errorf(\"please paste a non-empty API key\")\n\t}\n\n\tcheckCfg := m.cfg.Provider\n\tcheckCfg.APIKey = apiKey\n\tcheck := provider.CheckAvailability(context.Background(), checkCfg)\n\tif !check.Ready {\n\t\tm.llmConnected = false\n\t\tm.phase = \"error\"\n\t\tm.setStartupGuideStep(startupFieldAPIKey, startupGuideIssueHint(check))\n\t\treturn nil\n\t}\n\n\twrittenPath, saveErr := config.UpsertProviderAPIKey(m.startupGuide.ConfigPath, apiKey)\n\n\tif envName := strings.TrimSpace(checkCfg.APIKeyEnv); envName != \"\" {\n\t\tif err := os.Setenv(envName, apiKey); err != nil {\n\t\t\twarnSetenv(envName, err)\n\t\t}\n\t} else {\n\t\tif err := os.Setenv(\"BYTEMIND_API_KEY\", apiKey); err != nil {\n\t\t\twarnSetenv(\"BYTEMIND_API_KEY\", err)\n\t\t}\n\t}\n\n\tclient, err := provider.NewClient(checkCfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif m.runner != nil {\n\t\tm.runner.UpdateProvider(checkCfg, client)\n\t}\n\tm.cfg.Provider = checkCfg\n\tm.startupGuide.Active = false\n\tm.statusNote = \"Provider configured and verified. You can start chatting.\"\n\tm.llmConnected = true\n\tm.phase = \"idle\"\n\tif saveErr != nil {\n\t\tm.statusNote = \"Provider verified, but config save failed: \" + compact(saveErr.Error(), 80)\n\t} else if strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.statusNote = \"Provider configured and verified. Saved to \" + compact(writtenPath, 48)\n\t}\n\tm.syncInputStyle()\n\tm.input.Reset()\n\tm.clearPasteTransaction()\n\tm.clearVirtualPasteParts()\n\treturn nil\n}\n\nfunc (m *model) applyStartupConfigField(field, value string) error {\n\tfield = strings.TrimSpace(field)\n\tvalue = strings.TrimSpace(value)\n\tif value == \"\" {\n\t\treturn fmt.Errorf(\"%s cannot be empty\", field)\n\t}\n\tpersistValue := value\n\n\tswitch field {\n\tcase \"model\":\n\t\tm.cfg.Provider.Model = value\n\tcase \"base_url\":\n\t\tm.cfg.Provider.BaseURL = value\n\tcase \"type\":\n\t\tnormalized, ok := normalizeStartupProviderType(value)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"provider must be openai-compatible or anthropic\")\n\t\t}\n\t\tm.cfg.Provider.Type = normalized\n\t\tpersistValue = normalized\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n\n\twrittenPath, err := config.UpsertProviderField(m.startupGuide.ConfigPath, field, persistValue)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.startupGuide.ConfigPath = writtenPath\n\t}\n\treturn nil\n}\n\nfunc parseStartupConfigInput(raw string) (field, value string, ok bool) {\n\ttrimmed := strings.TrimSpace(raw)\n\tlower := strings.ToLower(trimmed)\n\tif lower == \"\" {\n\t\treturn \"\", \"\", false\n\t}\n\n\tparse := func(alias, normalized string) (string, string, bool) {\n\t\tfor _, sep := range []string{\"=\", \":\"} {\n\t\t\tprefix := alias + sep\n\t\t\tif strings.HasPrefix(lower, prefix) {\n\t\t\t\tval := strings.TrimSpace(trimmed[len(prefix):])\n\t\t\t\treturn normalized, val, true\n\t\t\t}\n\t\t}\n\t\treturn \"\", \"\", false\n\t}\n\n\tfor _, candidate := range []struct {\n\t\talias string\n\t\tnormalized string\n\t}{\n\t\t{alias: \"model\", normalized: \"model\"},\n\t\t{alias: \"base_url\", normalized: \"base_url\"},\n\t\t{alias: \"baseurl\", normalized: \"base_url\"},\n\t\t{alias: \"base-url\", normalized: \"base_url\"},\n\t\t{alias: \"provider\", normalized: \"type\"},\n\t\t{alias: \"type\", normalized: \"type\"},\n\t\t{alias: \"provider_type\", normalized: \"type\"},\n\t\t{alias: \"api_key\", normalized: \"api_key\"},\n\t\t{alias: \"apikey\", normalized: \"api_key\"},\n\t\t{alias: \"key\", normalized: \"api_key\"},\n\t} {\n\t\tif field, value, ok := parse(candidate.alias, candidate.normalized); ok {\n\t\t\treturn field, value, true\n\t\t}\n\t}\n\n\treturn \"\", \"\", false\n}\n\nfunc sanitizeAPIKeyInput(raw string) string {\n\tvalue := strings.TrimSpace(raw)\n\tvalue = strings.Trim(value, \"\\\"'\")\n\tlower := strings.ToLower(value)\n\tif strings.HasPrefix(lower, \"authorization: bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"authorization: bearer \"):])\n\t}\n\tif strings.HasPrefix(lower, \"bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"bearer \"):])\n\t}\n\treturn strings.TrimSpace(value)\n}\n\nfunc normalizeStartupProviderType(value string) (string, bool) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"openai-compatible\", \"openai_compatible\", \"openai\":\n\t\treturn \"openai-compatible\", true\n\tcase \"anthropic\":\n\t\treturn \"anthropic\", true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\nfunc isStartupGuideField(field string) bool {\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel, startupFieldAPIKey:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc startupNextField(current string) string {\n\tfor i, field := range startupFieldOrder {\n\t\tif field == current {\n\t\t\tif i+1 >= len(startupFieldOrder) {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn startupFieldOrder[i+1]\n\t\t}\n\t}\n\treturn startupFieldType\n}\n\nfunc startupFieldStep(field string) (int, int) {\n\tfor i, item := range startupFieldOrder {\n\t\tif item == field {\n\t\t\treturn i + 1, len(startupFieldOrder)\n\t\t}\n\t}\n\treturn 1, len(startupFieldOrder)\n}\n\nfunc startupFieldName(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"provider\"\n\tcase startupFieldBaseURL:\n\t\treturn \"base_url\"\n\tcase startupFieldModel:\n\t\treturn \"model\"\n\tcase startupFieldAPIKey:\n\t\treturn \"api_key\"\n\tdefault:\n\t\treturn field\n\t}\n}\n\nfunc startupProviderDefaultBaseURL(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc startupProviderDefaultModel(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"GPT-5.4\"\n\t}\n}\n\nfunc (m model) startupCurrentValue(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Type)\n\tcase startupFieldBaseURL:\n\t\treturn strings.TrimSpace(m.cfg.Provider.BaseURL)\n\tcase startupFieldModel:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Model)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (m *model) resolveStartupFieldValue(field, rawInput string) (string, error) {\n\tvalue := strings.TrimSpace(rawInput)\n\tif value != \"\" {\n\t\treturn value, nil\n\t}\n\n\tcurrent := m.startupCurrentValue(field)\n\tif current != \"\" {\n\t\treturn current, nil\n\t}\n\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"openai-compatible\", nil\n\tcase startupFieldBaseURL:\n\t\treturn startupProviderDefaultBaseURL(m.cfg.Provider.Type), nil\n\tcase startupFieldModel:\n\t\tif fallback := startupProviderDefaultModel(m.cfg.Provider.Type); fallback != \"\" {\n\t\t\treturn fallback, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"please enter model name for provider %s\", strings.TrimSpace(m.cfg.Provider.Type))\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"%s cannot be empty\", startupFieldName(field))\n\t}\n}\n\nfunc (m *model) initializeStartupGuide() {\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tm.setStartupGuideStep(field, \"\")\n}\n\nfunc (m *model) setStartupGuideStep(field, issue string) {\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tstep, total := startupFieldStep(field)\n\tfieldName := startupFieldName(field)\n\tif strings.TrimSpace(issue) == \"\" {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s.\", step, total, fieldName)\n\t} else {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s. %s\", step, total, fieldName, issue)\n\t}\n\tm.statusNote = m.startupGuide.Status\n\tm.startupGuide.CurrentField = field\n\tm.startupGuide.Lines = startupGuideStepLines(field, m.cfg, m.startupGuide.ConfigPath, issue)\n\tm.syncInputStyle()\n}\n\nfunc startupGuideStepLines(field string, cfg config.Config, configPath, issue string) []string {\n\tlines := make([]string, 0, 8)\n\tswitch field {\n\tcase startupFieldType:\n\t\tlines = append(lines, \"Enter provider: openai-compatible or anthropic.\")\n\tcase startupFieldBaseURL:\n\t\tlines = append(lines, \"Enter provider base_url.\")\n\t\tlines = append(lines, \"Example: https://api.deepseek.com\")\n\tcase startupFieldModel:\n\t\tlines = append(lines, \"Enter model name.\")\n\t\tlines = append(lines, \"Example: deepseek-chat or GPT-5.4\")\n\tcase startupFieldAPIKey:\n\t\tlines = append(lines, \"Paste API key and press Enter.\")\n\t\tlines = append(lines, \"Bytemind will verify it automatically.\")\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tcurrent := \"\"\n\t\tswitch field {\n\t\tcase startupFieldType:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Type)\n\t\tcase startupFieldBaseURL:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.BaseURL)\n\t\tcase startupFieldModel:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Model)\n\t\t}\n\t\tif current == \"\" {\n\t\t\tlines = append(lines, \"Press Enter to use default.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Press Enter to keep current: \"+current)\n\t\t}\n\t}\n\tif strings.TrimSpace(issue) != \"\" {\n\t\tlines = append(lines, \"Issue: \"+issue)\n\t}\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tlines = append(lines, \"Config file: \"+configPath)\n\t}\n\treturn lines\n}\n\nfunc startupGuideIssueHint(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"):\n\t\treturn \"No API key is configured yet.\"\n\tcase strings.Contains(reason, \"unauthorized\"):\n\t\treturn \"The API key was rejected by the provider.\"\n\tcase strings.Contains(reason, \"failed to reach\"):\n\t\treturn \"Cannot reach provider endpoint. Check proxy or network.\"\n\tcase strings.Contains(reason, \"not found\"):\n\t\treturn \"Provider endpoint path looks incorrect.\"\n\tdefault:\n\t\tif strings.TrimSpace(check.Reason) == \"\" {\n\t\t\treturn \"Provider check failed.\"\n\t\t}\n\t\treturn compact(strings.TrimSpace(check.Reason), 90)\n\t}\n}\n"}, "22bccda0f35cac23f6c835e66094ced6fdc312cf": {"t": 1785749489.9129412, "n": 10628, "h": "478d68e720852e2c", "c": "package tui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"bytemind/internal/config\"\n\t\"bytemind/internal/provider\"\n)\n\nfunc (m *model) handleStartupGuideSubmission(rawInput string) error {\n\trawInput = strings.TrimSpace(rawInput)\n\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tif explicitField, explicitValue, ok := parseStartupConfigInput(rawInput); ok {\n\t\tfield = explicitField\n\t\trawInput = explicitValue\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tvalue, err := m.resolveStartupFieldValue(field, rawInput)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := m.applyStartupConfigField(field, value); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnext := startupNextField(field)\n\t\tif next == \"\" {\n\t\t\tnext = startupFieldAPIKey\n\t\t}\n\t\tm.setStartupGuideStep(next, \"\")\n\t\tm.input.Reset()\n\t\tm.clearPasteTransaction()\n\t\tm.clearVirtualPasteParts()\n\t\treturn nil\n\tcase startupFieldAPIKey:\n\t\treturn m.verifyAndFinalizeStartupAPIKey(rawInput)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n}\n\nfunc (m *model) verifyAndFinalizeStartupAPIKey(rawInput string) error {\n\tapiKey := sanitizeAPIKeyInput(rawInput)\n\tif apiKey == \"\" {\n\t\treturn fmt.Errorf(\"please paste a non-empty API key\")\n\t}\n\n\tcheckCfg := m.cfg.Provider\n\tcheckCfg.APIKey = apiKey\n\tcheck := provider.CheckAvailability(context.Background(), checkCfg)\n\tif !check.Ready {\n\t\tm.llmConnected = false\n\t\tm.phase = \"error\"\n\t\tm.setStartupGuideStep(startupFieldAPIKey, startupGuideIssueHint(check))\n\t\treturn nil\n\t}\n\n\twrittenPath, saveErr := config.UpsertProviderAPIKey(m.startupGuide.ConfigPath, apiKey)\n\n\tif envName := strings.TrimSpace(checkCfg.APIKeyEnv); envName != \"\" {\n\t\tif err := os.Setenv(envName, apiKey); err != nil {\n\t\t\twarnSetenv(envName, err)\n\t\t}\n\t} else {\n\t\tif err := os.Setenv(\"BYTEMIND_API_KEY\", apiKey); err != nil {\n\t\t\twarnSetenv(\"BYTEMIND_API_KEY\", err)\n\t\t}\n\t}\n\n\tclient, err := provider.NewClient(checkCfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif m.runner != nil {\n\t\tm.runner.UpdateProvider(checkCfg, client)\n\t}\n\tm.cfg.Provider = checkCfg\n\tm.startupGuide.Active = false\n\tm.statusNote = \"Provider configured and verified. You can start chatting.\"\n\tm.llmConnected = true\n\tm.phase = \"idle\"\n\tif saveErr != nil {\n\t\tm.statusNote = \"Provider verified, but config save failed: \" + compact(saveErr.Error(), 80)\n\t} else if strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.statusNote = \"Provider configured and verified. Saved to \" + compact(writtenPath, 48)\n\t}\n\tm.syncInputStyle()\n\tm.input.Reset()\n\tm.clearPasteTransaction()\n\tm.clearVirtualPasteParts()\n\treturn nil\n}\n\nfunc (m *model) applyStartupConfigField(field, value string) error {\n\tfield = strings.TrimSpace(field)\n\tvalue = strings.TrimSpace(value)\n\tif value == \"\" {\n\t\treturn fmt.Errorf(\"%s cannot be empty\", field)\n\t}\n\tpersistValue := value\n\n\tswitch field {\n\tcase \"model\":\n\t\tm.cfg.Provider.Model = value\n\tcase \"base_url\":\n\t\tm.cfg.Provider.BaseURL = value\n\tcase \"type\":\n\t\tnormalized, ok := normalizeStartupProviderType(value)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"provider must be openai-compatible or anthropic\")\n\t\t}\n\t\tm.cfg.Provider.Type = normalized\n\t\tpersistValue = normalized\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n\n\twrittenPath, err := config.UpsertProviderField(m.startupGuide.ConfigPath, field, persistValue)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.startupGuide.ConfigPath = writtenPath\n\t}\n\treturn nil\n}\n\nfunc parseStartupConfigInput(raw string) (field, value string, ok bool) {\n\ttrimmed := strings.TrimSpace(raw)\n\tlower := strings.ToLower(trimmed)\n\tif lower == \"\" {\n\t\treturn \"\", \"\", false\n\t}\n\n\tparse := func(alias, normalized string) (string, string, bool) {\n\t\tfor _, sep := range []string{\"=\", \":\"} {\n\t\t\tprefix := alias + sep\n\t\t\tif strings.HasPrefix(lower, prefix) {\n\t\t\t\tval := strings.TrimSpace(trimmed[len(prefix):])\n\t\t\t\treturn normalized, val, true\n\t\t\t}\n\t\t}\n\t\treturn \"\", \"\", false\n\t}\n\n\tfor _, candidate := range []struct {\n\t\talias string\n\t\tnormalized string\n\t}{\n\t\t{alias: \"model\", normalized: \"model\"},\n\t\t{alias: \"base_url\", normalized: \"base_url\"},\n\t\t{alias: \"baseurl\", normalized: \"base_url\"},\n\t\t{alias: \"base-url\", normalized: \"base_url\"},\n\t\t{alias: \"provider\", normalized: \"type\"},\n\t\t{alias: \"type\", normalized: \"type\"},\n\t\t{alias: \"provider_type\", normalized: \"type\"},\n\t\t{alias: \"api_key\", normalized: \"api_key\"},\n\t\t{alias: \"apikey\", normalized: \"api_key\"},\n\t\t{alias: \"key\", normalized: \"api_key\"},\n\t} {\n\t\tif field, value, ok := parse(candidate.alias, candidate.normalized); ok {\n\t\t\treturn field, value, true\n\t\t}\n\t}\n\n\treturn \"\", \"\", false\n}\n\nfunc sanitizeAPIKeyInput(raw string) string {\n\tvalue := strings.TrimSpace(raw)\n\tvalue = strings.Trim(value, \"\\\"'\")\n\tlower := strings.ToLower(value)\n\tif strings.HasPrefix(lower, \"authorization: bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"authorization: bearer \"):])\n\t}\n\tif strings.HasPrefix(lower, \"bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"bearer \"):])\n\t}\n\treturn strings.TrimSpace(value)\n}\n\nfunc normalizeStartupProviderType(value string) (string, bool) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"openai-compatible\", \"openai_compatible\", \"openai\":\n\t\treturn \"openai-compatible\", true\n\tcase \"anthropic\":\n\t\treturn \"anthropic\", true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\nfunc isStartupGuideField(field string) bool {\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel, startupFieldAPIKey:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc startupNextField(current string) string {\n\tfor i, field := range startupFieldOrder {\n\t\tif field == current {\n\t\t\tif i+1 >= len(startupFieldOrder) {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn startupFieldOrder[i+1]\n\t\t}\n\t}\n\treturn startupFieldType\n}\n\nfunc startupFieldStep(field string) (int, int) {\n\tfor i, item := range startupFieldOrder {\n\t\tif item == field {\n\t\t\treturn i + 1, len(startupFieldOrder)\n\t\t}\n\t}\n\treturn 1, len(startupFieldOrder)\n}\n\nfunc startupFieldName(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"provider\"\n\tcase startupFieldBaseURL:\n\t\treturn \"base_url\"\n\tcase startupFieldModel:\n\t\treturn \"model\"\n\tcase startupFieldAPIKey:\n\t\treturn \"api_key\"\n\tdefault:\n\t\treturn field\n\t}\n}\n\nfunc startupProviderDefaultBaseURL(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc startupProviderDefaultModel(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"GPT-5.4\"\n\t}\n}\n\nfunc (m model) startupCurrentValue(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Type)\n\tcase startupFieldBaseURL:\n\t\treturn strings.TrimSpace(m.cfg.Provider.BaseURL)\n\tcase startupFieldModel:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Model)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (m *model) resolveStartupFieldValue(field, rawInput string) (string, error) {\n\tvalue := strings.TrimSpace(rawInput)\n\tif value != \"\" {\n\t\treturn value, nil\n\t}\n\n\tcurrent := m.startupCurrentValue(field)\n\tif current != \"\" {\n\t\treturn current, nil\n\t}\n\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"openai-compatible\", nil\n\tcase startupFieldBaseURL:\n\t\treturn startupProviderDefaultBaseURL(m.cfg.Provider.Type), nil\n\tcase startupFieldModel:\n\t\tif fallback := startupProviderDefaultModel(m.cfg.Provider.Type); fallback != \"\" {\n\t\t\treturn fallback, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"please enter model name for provider %s\", strings.TrimSpace(m.cfg.Provider.Type))\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"%s cannot be empty\", startupFieldName(field))\n\t}\n}\n\nfunc (m *model) initializeStartupGuide() {\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tm.setStartupGuideStep(field, \"\")\n}\n\nfunc (m *model) setStartupGuideStep(field, issue string) {\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tstep, total := startupFieldStep(field)\n\tfieldName := startupFieldName(field)\n\tif strings.TrimSpace(issue) == \"\" {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s.\", step, total, fieldName)\n\t} else {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s. %s\", step, total, fieldName, issue)\n\t}\n\tm.statusNote = m.startupGuide.Status\n\tm.startupGuide.CurrentField = field\n\tm.startupGuide.Lines = startupGuideStepLines(field, m.cfg, m.startupGuide.ConfigPath, issue)\n\tm.syncInputStyle()\n}\n\nfunc startupGuideStepLines(field string, cfg config.Config, configPath, issue string) []string {\n\tlines := make([]string, 0, 8)\n\tswitch field {\n\tcase startupFieldType:\n\t\tlines = append(lines, \"Enter provider: openai-compatible or anthropic.\")\n\tcase startupFieldBaseURL:\n\t\tlines = append(lines, \"Enter provider base_url.\")\n\t\tlines = append(lines, \"Example: https://api.deepseek.com\")\n\tcase startupFieldModel:\n\t\tlines = append(lines, \"Enter model name.\")\n\t\tlines = append(lines, \"Example: deepseek-chat or GPT-5.4\")\n\tcase startupFieldAPIKey:\n\t\tlines = append(lines, \"Paste API key and press Enter.\")\n\t\tlines = append(lines, \"Bytemind will verify it automatically.\")\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tcurrent := \"\"\n\t\tswitch field {\n\t\tcase startupFieldType:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Type)\n\t\tcase startupFieldBaseURL:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.BaseURL)\n\t\tcase startupFieldModel:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Model)\n\t\t}\n\t\tif current == \"\" {\n\t\t\tlines = append(lines, \"Press Enter to use default.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Press Enter to keep current: \"+current)\n\t\t}\n\t}\n\tif strings.TrimSpace(issue) != \"\" {\n\t\tlines = append(lines, \"Issue: \"+issue)\n\t}\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tlines = append(lines, \"Config file: \"+configPath)\n\t}\n\treturn lines\n}\n\nfunc startupGuideIssueHint(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"):\n\t\treturn \"No API key is configured yet.\"\n\tcase strings.Contains(reason, \"unauthorized\"):\n\t\treturn \"The API key was rejected by the provider.\"\n\tcase strings.Contains(reason, \"failed to reach\"):\n\t\treturn \"Cannot reach provider endpoint. Check proxy or network.\"\n\tcase strings.Contains(reason, \"not found\"):\n\t\treturn \"Provider endpoint path looks incorrect.\"\n\tdefault:\n\t\tif strings.TrimSpace(check.Reason) == \"\" {\n\t\t\treturn \"Provider check failed.\"\n\t\t}\n\t\treturn compact(strings.TrimSpace(check.Reason), 90)\n\t}\n}\n"}, "8d56d13505c3525d5a1c6faf85083b35fc365954": {"t": 1785749490.99545, "n": 10516, "h": "78db4f0fe99ade8c", "c": "package tui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"bytemind/internal/config\"\n\t\"bytemind/internal/provider\"\n)\n\nfunc (m *model) handleStartupGuideSubmission(rawInput string) error {\n\trawInput = strings.TrimSpace(rawInput)\n\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tif explicitField, explicitValue, ok := parseStartupConfigInput(rawInput); ok {\n\t\tfield = explicitField\n\t\trawInput = explicitValue\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tvalue, err := m.resolveStartupFieldValue(field, rawInput)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := m.applyStartupConfigField(field, value); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnext := startupNextField(field)\n\t\tif next == \"\" {\n\t\t\tnext = startupFieldAPIKey\n\t\t}\n\t\tm.setStartupGuideStep(next, \"\")\n\t\tm.input.Reset()\n\t\treturn nil\n\tcase startupFieldAPIKey:\n\t\treturn m.verifyAndFinalizeStartupAPIKey(rawInput)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n}\n\nfunc (m *model) verifyAndFinalizeStartupAPIKey(rawInput string) error {\n\tapiKey := sanitizeAPIKeyInput(rawInput)\n\tif apiKey == \"\" {\n\t\treturn fmt.Errorf(\"please paste a non-empty API key\")\n\t}\n\n\tcheckCfg := m.cfg.Provider\n\tcheckCfg.APIKey = apiKey\n\tcheck := provider.CheckAvailability(context.Background(), checkCfg)\n\tif !check.Ready {\n\t\tm.llmConnected = false\n\t\tm.phase = \"error\"\n\t\tm.setStartupGuideStep(startupFieldAPIKey, startupGuideIssueHint(check))\n\t\treturn nil\n\t}\n\n\twrittenPath, saveErr := config.UpsertProviderAPIKey(m.startupGuide.ConfigPath, apiKey)\n\n\tif envName := strings.TrimSpace(checkCfg.APIKeyEnv); envName != \"\" {\n\t\tif err := os.Setenv(envName, apiKey); err != nil {\n\t\t\twarnSetenv(envName, err)\n\t\t}\n\t} else {\n\t\tif err := os.Setenv(\"BYTEMIND_API_KEY\", apiKey); err != nil {\n\t\t\twarnSetenv(\"BYTEMIND_API_KEY\", err)\n\t\t}\n\t}\n\n\tclient, err := provider.NewClient(checkCfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif m.runner != nil {\n\t\tm.runner.UpdateProvider(checkCfg, client)\n\t}\n\tm.cfg.Provider = checkCfg\n\tm.startupGuide.Active = false\n\tm.statusNote = \"Provider configured and verified. You can start chatting.\"\n\tm.llmConnected = true\n\tm.phase = \"idle\"\n\tif saveErr != nil {\n\t\tm.statusNote = \"Provider verified, but config save failed: \" + compact(saveErr.Error(), 80)\n\t} else if strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.statusNote = \"Provider configured and verified. Saved to \" + compact(writtenPath, 48)\n\t}\n\tm.syncInputStyle()\n\tm.input.Reset()\n\treturn nil\n}\n\nfunc (m *model) applyStartupConfigField(field, value string) error {\n\tfield = strings.TrimSpace(field)\n\tvalue = strings.TrimSpace(value)\n\tif value == \"\" {\n\t\treturn fmt.Errorf(\"%s cannot be empty\", field)\n\t}\n\tpersistValue := value\n\n\tswitch field {\n\tcase \"model\":\n\t\tm.cfg.Provider.Model = value\n\tcase \"base_url\":\n\t\tm.cfg.Provider.BaseURL = value\n\tcase \"type\":\n\t\tnormalized, ok := normalizeStartupProviderType(value)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"provider must be openai-compatible or anthropic\")\n\t\t}\n\t\tm.cfg.Provider.Type = normalized\n\t\tpersistValue = normalized\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported setup field: %s\", field)\n\t}\n\n\twrittenPath, err := config.UpsertProviderField(m.startupGuide.ConfigPath, field, persistValue)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif strings.TrimSpace(writtenPath) != \"\" {\n\t\tm.startupGuide.ConfigPath = writtenPath\n\t}\n\treturn nil\n}\n\nfunc parseStartupConfigInput(raw string) (field, value string, ok bool) {\n\ttrimmed := strings.TrimSpace(raw)\n\tlower := strings.ToLower(trimmed)\n\tif lower == \"\" {\n\t\treturn \"\", \"\", false\n\t}\n\n\tparse := func(alias, normalized string) (string, string, bool) {\n\t\tfor _, sep := range []string{\"=\", \":\"} {\n\t\t\tprefix := alias + sep\n\t\t\tif strings.HasPrefix(lower, prefix) {\n\t\t\t\tval := strings.TrimSpace(trimmed[len(prefix):])\n\t\t\t\treturn normalized, val, true\n\t\t\t}\n\t\t}\n\t\treturn \"\", \"\", false\n\t}\n\n\tfor _, candidate := range []struct {\n\t\talias string\n\t\tnormalized string\n\t}{\n\t\t{alias: \"model\", normalized: \"model\"},\n\t\t{alias: \"base_url\", normalized: \"base_url\"},\n\t\t{alias: \"baseurl\", normalized: \"base_url\"},\n\t\t{alias: \"base-url\", normalized: \"base_url\"},\n\t\t{alias: \"provider\", normalized: \"type\"},\n\t\t{alias: \"type\", normalized: \"type\"},\n\t\t{alias: \"provider_type\", normalized: \"type\"},\n\t\t{alias: \"api_key\", normalized: \"api_key\"},\n\t\t{alias: \"apikey\", normalized: \"api_key\"},\n\t\t{alias: \"key\", normalized: \"api_key\"},\n\t} {\n\t\tif field, value, ok := parse(candidate.alias, candidate.normalized); ok {\n\t\t\treturn field, value, true\n\t\t}\n\t}\n\n\treturn \"\", \"\", false\n}\n\nfunc sanitizeAPIKeyInput(raw string) string {\n\tvalue := strings.TrimSpace(raw)\n\tvalue = strings.Trim(value, \"\\\"'\")\n\tlower := strings.ToLower(value)\n\tif strings.HasPrefix(lower, \"authorization: bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"authorization: bearer \"):])\n\t}\n\tif strings.HasPrefix(lower, \"bearer \") {\n\t\tvalue = strings.TrimSpace(value[len(\"bearer \"):])\n\t}\n\treturn strings.TrimSpace(value)\n}\n\nfunc normalizeStartupProviderType(value string) (string, bool) {\n\tswitch strings.ToLower(strings.TrimSpace(value)) {\n\tcase \"openai-compatible\", \"openai_compatible\", \"openai\":\n\t\treturn \"openai-compatible\", true\n\tcase \"anthropic\":\n\t\treturn \"anthropic\", true\n\tdefault:\n\t\treturn \"\", false\n\t}\n}\n\nfunc isStartupGuideField(field string) bool {\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel, startupFieldAPIKey:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc startupNextField(current string) string {\n\tfor i, field := range startupFieldOrder {\n\t\tif field == current {\n\t\t\tif i+1 >= len(startupFieldOrder) {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn startupFieldOrder[i+1]\n\t\t}\n\t}\n\treturn startupFieldType\n}\n\nfunc startupFieldStep(field string) (int, int) {\n\tfor i, item := range startupFieldOrder {\n\t\tif item == field {\n\t\t\treturn i + 1, len(startupFieldOrder)\n\t\t}\n\t}\n\treturn 1, len(startupFieldOrder)\n}\n\nfunc startupFieldName(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"provider\"\n\tcase startupFieldBaseURL:\n\t\treturn \"base_url\"\n\tcase startupFieldModel:\n\t\treturn \"model\"\n\tcase startupFieldAPIKey:\n\t\treturn \"api_key\"\n\tdefault:\n\t\treturn field\n\t}\n}\n\nfunc startupProviderDefaultBaseURL(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"https://api.anthropic.com\"\n\tdefault:\n\t\treturn \"https://api.openai.com/v1\"\n\t}\n}\n\nfunc startupProviderDefaultModel(providerType string) string {\n\tswitch strings.ToLower(strings.TrimSpace(providerType)) {\n\tcase \"anthropic\":\n\t\treturn \"\"\n\tdefault:\n\t\treturn \"GPT-5.4\"\n\t}\n}\n\nfunc (m model) startupCurrentValue(field string) string {\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Type)\n\tcase startupFieldBaseURL:\n\t\treturn strings.TrimSpace(m.cfg.Provider.BaseURL)\n\tcase startupFieldModel:\n\t\treturn strings.TrimSpace(m.cfg.Provider.Model)\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc (m *model) resolveStartupFieldValue(field, rawInput string) (string, error) {\n\tvalue := strings.TrimSpace(rawInput)\n\tif value != \"\" {\n\t\treturn value, nil\n\t}\n\n\tcurrent := m.startupCurrentValue(field)\n\tif current != \"\" {\n\t\treturn current, nil\n\t}\n\n\tswitch field {\n\tcase startupFieldType:\n\t\treturn \"openai-compatible\", nil\n\tcase startupFieldBaseURL:\n\t\treturn startupProviderDefaultBaseURL(m.cfg.Provider.Type), nil\n\tcase startupFieldModel:\n\t\tif fallback := startupProviderDefaultModel(m.cfg.Provider.Type); fallback != \"\" {\n\t\t\treturn fallback, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"please enter model name for provider %s\", strings.TrimSpace(m.cfg.Provider.Type))\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"%s cannot be empty\", startupFieldName(field))\n\t}\n}\n\nfunc (m *model) initializeStartupGuide() {\n\tfield := strings.TrimSpace(m.startupGuide.CurrentField)\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tm.setStartupGuideStep(field, \"\")\n}\n\nfunc (m *model) setStartupGuideStep(field, issue string) {\n\tif !isStartupGuideField(field) {\n\t\tfield = startupFieldType\n\t}\n\tstep, total := startupFieldStep(field)\n\tfieldName := startupFieldName(field)\n\tif strings.TrimSpace(issue) == \"\" {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s.\", step, total, fieldName)\n\t} else {\n\t\tm.startupGuide.Status = fmt.Sprintf(\"Step %d/%d: set %s. %s\", step, total, fieldName, issue)\n\t}\n\tm.statusNote = m.startupGuide.Status\n\tm.startupGuide.CurrentField = field\n\tm.startupGuide.Lines = startupGuideStepLines(field, m.cfg, m.startupGuide.ConfigPath, issue)\n\tm.syncInputStyle()\n}\n\nfunc startupGuideStepLines(field string, cfg config.Config, configPath, issue string) []string {\n\tlines := make([]string, 0, 8)\n\tswitch field {\n\tcase startupFieldType:\n\t\tlines = append(lines, \"Enter provider: openai-compatible or anthropic.\")\n\tcase startupFieldBaseURL:\n\t\tlines = append(lines, \"Enter provider base_url.\")\n\t\tlines = append(lines, \"Example: https://api.deepseek.com\")\n\tcase startupFieldModel:\n\t\tlines = append(lines, \"Enter model name.\")\n\t\tlines = append(lines, \"Example: deepseek-chat or GPT-5.4\")\n\tcase startupFieldAPIKey:\n\t\tlines = append(lines, \"Paste API key and press Enter.\")\n\t\tlines = append(lines, \"Bytemind will verify it automatically.\")\n\t}\n\n\tswitch field {\n\tcase startupFieldType, startupFieldBaseURL, startupFieldModel:\n\t\tcurrent := \"\"\n\t\tswitch field {\n\t\tcase startupFieldType:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Type)\n\t\tcase startupFieldBaseURL:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.BaseURL)\n\t\tcase startupFieldModel:\n\t\t\tcurrent = strings.TrimSpace(cfg.Provider.Model)\n\t\t}\n\t\tif current == \"\" {\n\t\t\tlines = append(lines, \"Press Enter to use default.\")\n\t\t} else {\n\t\t\tlines = append(lines, \"Press Enter to keep current: \"+current)\n\t\t}\n\t}\n\tif strings.TrimSpace(issue) != \"\" {\n\t\tlines = append(lines, \"Issue: \"+issue)\n\t}\n\tif strings.TrimSpace(configPath) != \"\" {\n\t\tlines = append(lines, \"Config file: \"+configPath)\n\t}\n\treturn lines\n}\n\nfunc startupGuideIssueHint(check provider.Availability) string {\n\treason := strings.ToLower(strings.TrimSpace(check.Reason))\n\tswitch {\n\tcase strings.Contains(reason, \"missing api key\"):\n\t\treturn \"No API key is configured yet.\"\n\tcase strings.Contains(reason, \"unauthorized\"):\n\t\treturn \"The API key was rejected by the provider.\"\n\tcase strings.Contains(reason, \"failed to reach\"):\n\t\treturn \"Cannot reach provider endpoint. Check proxy or network.\"\n\tcase strings.Contains(reason, \"not found\"):\n\t\treturn \"Provider endpoint path looks incorrect.\"\n\tdefault:\n\t\tif strings.TrimSpace(check.Reason) == \"\" {\n\t\t\treturn \"Provider check failed.\"\n\t\t}\n\t\treturn compact(strings.TrimSpace(check.Reason), 90)\n\t}\n}\n"}}}}