1 line
1.2 MiB
Plaintext
1 line
1.2 MiB
Plaintext
{"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\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\n\n\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\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\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</Tab>\n\n<Tab title=\"MacOS\">\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</Tab>\n</Tabs>\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n\n<Tab title=\"macOS\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n</Tabs>\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\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\n\n\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\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\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</Tab>\n\n<Tab title=\"MacOS\">\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</Tab>\n</Tabs>\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n\n<Tab title=\"macOS\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n</Tabs>\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\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\n\n\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\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\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</Tab>\n\n<Tab title=\"MacOS\">\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</Tab>\n</Tabs>\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n\n<Tab title=\"macOS\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n</Tabs>\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\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\n\n\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\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\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</Tab>\n\n<Tab title=\"MacOS\">\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</Tab>\n</Tabs>\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\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\n\n\n\n不要把 API Key 发给别人,也不要提交到公开仓库。对新手来说,先写进本机 `~/.bytemind/config.json` 最直观;熟悉后可以改用环境变量。\n\n## 第三步:解读 API 文档\n\n打开 DeepSeek 的 [首次调用 API](https://api-docs.deepseek.com/zh-cn/) 文档,重点只看这几项:\n\n\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\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</Tab>\n\n<Tab title=\"MacOS\">\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</Tab>\n</Tabs>\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\n\n```powershell\n# 临时(仅当前窗口有效):\n$env:DEEPSEEK_API_KEY = \"sk-...\"\n\n# 永久(重启电脑后依然有效):\n[Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\", \"sk-...\", \"User\")\n# 执行后需重启终端窗口。\n```\n\n</Tab>\n\n<Tab title=\"Linux\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n\n<Tab title=\"macOS\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n</Tabs>\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\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\n\n\n\n不要把 API Key 发给别人,也不要提交到公开仓库。对新手来说,先写进本机 `~/.bytemind/config.json` 最直观;熟悉后可以改用环境变量。\n\n## 第三步:解读 API 文档\n\n打开 DeepSeek 的 [首次调用 API](https://api-docs.deepseek.com/zh-cn/) 文档,重点只看这几项:\n\n\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\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</Tab>\n\n<Tab title=\"MacOS\">\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</Tab>\n</Tabs>\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\n\n```powershell\n# 临时(仅当前窗口有效):\n$env:DEEPSEEK_API_KEY = \"sk-...\"\n\n# 永久(重启电脑后依然有效):\n[Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\", \"sk-...\", \"User\")\n# 执行后需重启终端窗口。\n```\n\n</Tab>\n\n<Tab title=\"Linux\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n\n<Tab title=\"macOS\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n</Tabs>\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\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\n\n\n\n不要把 API Key 发给别人,也不要提交到公开仓库。对新手来说,先写进本机 `~/.bytemind/config.json` 最直观;熟悉后可以改用环境变量。\n\n## 第三步:解读 API 文档\n\n打开 DeepSeek 的 [首次调用 API](https://api-docs.deepseek.com/zh-cn/) 文档,重点只看这几项:\n\n\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\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</Tab>\n\n<Tab title=\"MacOS\">\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</Tab>\n</Tabs>\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\n\n```powershell\n# 临时(仅当前窗口有效):\n$env:DEEPSEEK_API_KEY = \"sk-...\"\n\n# 永久(重启电脑后依然有效):\n[Environment]::SetEnvironmentVariable(\"DEEPSEEK_API_KEY\", \"sk-...\", \"User\")\n# 执行后需重启终端窗口。\n```\n\n</Tab>\n\n<Tab title=\"Linux\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n\n<Tab title=\"macOS\">\n\n```bash\nexport DEEPSEEK_API_KEY=\"sk-...\"\n```\n\n</Tab>\n</Tabs>\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\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\n\n\n\n不要把 API Key 发给别人,也不要提交到公开仓库。对新手来说,先写进本机 `~/.bytemind/config.json` 最直观;熟悉后可以改用环境变量。\n\n## 第三步:解读 API 文档\n\n打开 DeepSeek 的 [首次调用 API](https://api-docs.deepseek.com/zh-cn/) 文档,重点只看这几项:\n\n\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<Tabs default-tab=\"PowerShell\">\n<Tab title=\"PowerShell\">\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</Tab>\n\n<Tab title=\"Linux\">\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</Tab>\n\n<Tab title=\"MacOS\">\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</Tab>\n</Tabs>\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"}}}} |