diff --git a/.agents/skills/agent-core-dev/flags.md b/.agents/skills/agent-core-dev/flags.md index e824699ca68..60349fa73e9 100644 --- a/.agents/skills/agent-core-dev/flags.md +++ b/.agents/skills/agent-core-dev/flags.md @@ -24,9 +24,9 @@ Gate not-yet-public features behind `IFlagService.enabled(id)`, per the reposito Highest wins; env is read live on every call (nothing cached): -1. Master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on. -2. Per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off. -3. `[experimental]` config section per-flag override. +1. Per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off. +2. `[experimental]` config section per-flag override. +3. Master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on. 4. Registry `default`. `explain(id)` returns the winning `source` (`master-env` | `env` | `config` | `default`) plus the effective `configValue`. `explain(id)` returns `undefined` (and `enabled(id)` returns `false`) for an id that no domain has registered. diff --git a/.changeset/flag-config-over-master-env.md b/.changeset/flag-config-over-master-env.md new file mode 100644 index 00000000000..468827476c9 --- /dev/null +++ b/.changeset/flag-config-over-master-env.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Honor explicit `[experimental]` config entries over the `KIMI_CODE_EXPERIMENTAL_FLAG` master switch, so a flag set to `false` in `config.toml` stays off; per-feature `KIMI_CODE_EXPERIMENTAL_` variables still override both. diff --git a/AGENTS.md b/AGENTS.md index fd94bcdfaf6..d95144c9d0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo ## Experimental Features -- Gate a not-yet-public feature behind an experimental flag. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Release by flipping the entry's `default` to `true`. +- Gate a not-yet-public feature behind an experimental flag. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Precedence is per-flag env > `[experimental]` config > master env > the flag's `default`. Release by flipping the entry's `default` to `true`. - `packages/agent-core` (v1): add the flag to the central registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`. - `packages/agent-core-v2` and kap-server modules: there is no central catalog — declare the flag in the owning domain via `registerFlagDefinition` at import time (see `packages/agent-core-v2/docs/flag.md`), then check it with `IFlagService.enabled(id)`. Current search-index-separation flags: `persistence_minidb_readmodel` (session read model, default on) and `search_worker` (global search worker host, default on). diff --git a/docs/en/configuration/env-vars.md b/docs/en/configuration/env-vars.md index a45f60ac978..8211d180d07 100644 --- a/docs/en/configuration/env-vars.md +++ b/docs/en/configuration/env-vars.md @@ -150,7 +150,7 @@ Switches that control the behavior of subsystems such as telemetry, background t | `KIMI_CODE_IDENTITY_SLUG` | Protocol identifier for the `User-Agent` product token sent to third-party providers and the MCP client name; takes higher priority than `[identity] slug`. Derived from the name when unset | Any non-empty string; normalized to lowercase with non-alphanumeric runs folded to `-` | | `KIMI_CODE_BUILTIN_PRODUCT_SKILLS` | Whether the built-in skills documenting Kimi Code itself are offered to the model; takes higher priority than `builtin_product_skills` in `config.toml` (default enabled) | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `KIMI_CODE_TUI_FULL_SCREEN` | Enable the experimental fullscreen alternate-screen UI: scrollable transcript viewport, mouse text selection, clickable links, and Ctrl-Shift-F transcript search | `1` enables it; anything else keeps the regular inline UI | -| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | The [subagent model pool](./config-files.md#subagent-model-pool) is enabled by default in every launch mode, including the interactive TUI; set a falsy value to disable it; the master `KIMI_CODE_EXPERIMENTAL_FLAG=1` force-enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | +| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | The [subagent model pool](./config-files.md#subagent-model-pool) is enabled by default in every launch mode, including the interactive TUI; set a falsy value to disable it; the master `KIMI_CODE_EXPERIMENTAL_FLAG=1` also enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `KIMI_CODE_EXPERIMENTAL_SUBAGENT_FORK` | Enable the experimental `fork` parameter on the `Agent` and `AgentSwarm` tools, letting the model start a subagent with a snapshot of the calling agent's conversation history instead of an empty context; the master `KIMI_CODE_EXPERIMENTAL_FLAG=1` also enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `KIMI_MCP_STARTUP_TIMEOUT_MS` | Global default connection timeout (ms) for all MCP servers; takes higher priority than `[mcp] startup_timeout_ms` in `config.toml`, but a per-server `startupTimeoutMs` in `mcp.json` still wins (default `30000`) | Integer from `1` to `2147483647`; invalid values are ignored | | `KIMI_MCP_TOOL_TIMEOUT_MS` | Global default single tool-call timeout (ms) for all MCP servers; takes higher priority than `[mcp] tool_timeout_ms` in `config.toml`, but a per-server `toolTimeoutMs` in `mcp.json` still wins (default `60000`) | Integer from `1` to `2147483647`; invalid values are ignored | @@ -162,7 +162,7 @@ Switches that control the behavior of subsystems such as telemetry, background t | `KIMI_WEB_SEARCH_API_KEY` | API key of the web search (`WebSearch`) service; replaces both the configured API key and OAuth credential when set | Non-blank string; blank values are ignored | | `KIMI_WEB_FETCH_BASE_URL` | API URL of the web fetch (`FetchURL`) service; takes higher priority than `[services.moonshot_fetch] base_url`. Persisted credentials and custom headers are not forwarded to an env-selected endpoint. Without an env or config endpoint, signed-in users try the managed Kimi OAuth fetch service before direct local requests | Non-blank string; blank values are ignored | | `KIMI_WEB_FETCH_API_KEY` | API key of the web fetch (`FetchURL`) service; replaces both the configured API key and OAuth credential when set | Non-blank string; blank values are ignored | -| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; it does not select the agent engine | `1`, `true`, `yes`, `on` | +| `KIMI_CODE_EXPERIMENTAL_FLAG` | Enable all registered experimental features for this process; a per-feature `KIMI_CODE_EXPERIMENTAL_` variable or an explicit entry in the `[experimental]` section of `config.toml` takes precedence over it; it does not select the agent engine | `1`, `true`, `yes`, `on` | | `KIMI_CODE_LEGACY_FLAG` | Use the legacy `agent-core` engine for `kimi`, `kimi -p`, `kimi doctor`, `kimi acp`, `kimi export`, and `kimi provider`; these commands use `agent-core-v2` by default | `1`, `true`, `yes`, `on` | | `KIMI_SHELL_PATH` | Override the Git Bash path on Windows (used when auto-detection fails) | Absolute path | | `KIMI_MODEL_MAX_COMPLETION_TOKENS` | Hard cap on `max_completion_tokens` per LLM step; applies to the `kimi` provider only | Positive integer; `0` or negative disables clamping | diff --git a/docs/zh/configuration/env-vars.md b/docs/zh/configuration/env-vars.md index 55e9046ae77..41c6ad5f17c 100644 --- a/docs/zh/configuration/env-vars.md +++ b/docs/zh/configuration/env-vars.md @@ -150,7 +150,7 @@ kimi | `KIMI_CODE_IDENTITY_SLUG` | 协议标识,用于发给第三方 provider 的 `User-Agent` 产品名和 MCP 客户端名,优先级高于 `[identity] slug`。未设置时由名称派生 | 任意非空字符串;会转小写并将连续非字母数字字符折叠为 `-` | | `KIMI_CODE_BUILTIN_PRODUCT_SKILLS` | 是否向模型提供介绍 Kimi Code 自身的内置 Skills,优先级高于 `config.toml` 的 `builtin_product_skills`(默认开启) | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` | | `KIMI_CODE_TUI_FULL_SCREEN` | 启用实验性的 fullscreen alternate-screen 界面:可滚动的 transcript 视口、鼠标选择文本、可点击链接、Ctrl-Shift-F 搜索 | `1` 开启;其他值保持常规内联界面 | -| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | [subagent 模型池](./config-files.md#subagent-模型池) 默认启用,在包括交互式 TUI 在内的所有启动方式下生效;设为假值可禁用;master `KIMI_CODE_EXPERIMENTAL_FLAG=1` 会强制启用本功能 | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` | +| `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` | [subagent 模型池](./config-files.md#subagent-模型池) 默认启用,在包括交互式 TUI 在内的所有启动方式下生效;设为假值可禁用;master `KIMI_CODE_EXPERIMENTAL_FLAG=1` 也会启用本功能 | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` | | `KIMI_CODE_EXPERIMENTAL_SUBAGENT_FORK` | 在 `Agent` 和 `AgentSwarm` 工具上启用实验性的 `fork` 参数,让模型可以以调用方 Agent 对话历史的快照而不是空上下文启动 subagent;master `KIMI_CODE_EXPERIMENTAL_FLAG=1` 也会启用本功能 | 真值:`1`/`true`/`yes`/`on`;假值:`0`/`false`/`no`/`off` | | `KIMI_MCP_STARTUP_TIMEOUT_MS` | 所有 MCP server 的全局默认连接超时(毫秒);优先级高于 `config.toml` 的 `[mcp] startup_timeout_ms`,但低于 `mcp.json` 中单个 server 的 `startupTimeoutMs`(默认 `30000`) | `1` 到 `2147483647` 的整数;非法值被忽略 | | `KIMI_MCP_TOOL_TIMEOUT_MS` | 所有 MCP server 的全局默认单次工具调用超时(毫秒);优先级高于 `config.toml` 的 `[mcp] tool_timeout_ms`,但低于 `mcp.json` 中单个 server 的 `toolTimeoutMs`(默认 `60000`) | `1` 到 `2147483647` 的整数;非法值被忽略 | @@ -162,7 +162,7 @@ kimi | `KIMI_WEB_SEARCH_API_KEY` | 网页搜索(`WebSearch`)服务的 API 密钥;设置后同时替换配置中的 API 密钥和 OAuth 凭据 | 非空字符串;空白值被忽略 | | `KIMI_WEB_FETCH_BASE_URL` | 网页抓取(`FetchURL`)服务的 API URL;优先级高于 `[services.moonshot_fetch] base_url`。文件中持久化的凭据和自定义 header 不会发送到环境变量指定的端点。环境变量和配置都没有指定端点时,已登录用户会先尝试 Kimi OAuth 托管抓取服务,再回退到本地直接请求 | 非空字符串;空白值被忽略 | | `KIMI_WEB_FETCH_API_KEY` | 网页抓取(`FetchURL`)服务的 API 密钥;设置后同时替换配置中的 API 密钥和 OAuth 凭据 | 非空字符串;空白值被忽略 | -| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有已注册的实验功能;不用于选择 Agent 引擎 | `1`、`true`、`yes`、`on` | +| `KIMI_CODE_EXPERIMENTAL_FLAG` | 在当前进程启用所有已注册的实验功能;单个功能的 `KIMI_CODE_EXPERIMENTAL_` 变量或 `config.toml` 的 `[experimental]` 节中的显式配置优先于它;不用于选择 Agent 引擎 | `1`、`true`、`yes`、`on` | | `KIMI_CODE_LEGACY_FLAG` | 让 `kimi`、`kimi -p`、`kimi doctor`、`kimi acp`、`kimi export` 和 `kimi provider` 使用旧版 `agent-core` 引擎;这些命令默认使用 `agent-core-v2` | `1`、`true`、`yes`、`on` | | `KIMI_SHELL_PATH` | Windows 上覆盖 Git Bash 路径(自动探测失败时使用) | 绝对路径 | | `KIMI_MODEL_MAX_COMPLETION_TOKENS` | 单步 LLM 请求的 `max_completion_tokens` 硬上限,仅对 `kimi` 供应商生效 | 正整数;`0` 或负数禁用 clamp | diff --git a/packages/agent-core-v2/docs/flag.md b/packages/agent-core-v2/docs/flag.md index b8c7963427c..62201081265 100644 --- a/packages/agent-core-v2/docs/flag.md +++ b/packages/agent-core-v2/docs/flag.md @@ -24,9 +24,9 @@ Gates not-yet-public features behind `IFlagService.enabled(id)`, per the reposit Highest wins; env is read live on every call (nothing cached): -1. L1 master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on. -2. L2 per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off. -3. L3 `[experimental]` config section per-flag override. +1. L1 per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off. +2. L2 `[experimental]` config section per-flag override. +3. L3 master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on. 4. L4 registry `default`. `explain(id)` returns the winning `source` (`master-env` | `env` | `config` | `default`) plus the effective `configValue`. `explain(id)` returns `undefined` (and `enabled(id)` returns `false`) for an id that no domain has registered. diff --git a/packages/agent-core-v2/src/app/flag/flagService.ts b/packages/agent-core-v2/src/app/flag/flagService.ts index b8264b7bd65..4a41ea8bf8c 100644 --- a/packages/agent-core-v2/src/app/flag/flagService.ts +++ b/packages/agent-core-v2/src/app/flag/flagService.ts @@ -55,12 +55,12 @@ export class FlagService extends Disposable implements IFlagService { const def = this.registry.get(id); if (def === undefined) return undefined; const configValue = this.configOverrides[def.id]; - if (parseBooleanEnv(this.bootstrap.getEnv(MASTER_ENV)) === true) { - return this.state(def, true, 'master-env', configValue); - } const override = parseBooleanEnv(this.bootstrap.getEnv(def.env)); if (override !== undefined) return this.state(def, override, 'env', configValue); if (configValue !== undefined) return this.state(def, configValue, 'config', configValue); + if (parseBooleanEnv(this.bootstrap.getEnv(MASTER_ENV)) === true) { + return this.state(def, true, 'master-env', configValue); + } return this.state(def, def.default, 'default', undefined); } diff --git a/packages/agent-core-v2/test/app/flag/flag.test.ts b/packages/agent-core-v2/test/app/flag/flag.test.ts index 2749afcb059..4f2188920d4 100644 --- a/packages/agent-core-v2/test/app/flag/flag.test.ts +++ b/packages/agent-core-v2/test/app/flag/flag.test.ts @@ -117,9 +117,10 @@ describe('FlagService', () => { expect(state?.configValue).toBe(false); }); - it('lets per-feature env override config', async () => { + it('lets per-feature env override config and the master env', async () => { const { config, flags } = makeFlags({ KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG: 'true', + [MASTER_ENV]: '1', }); await config.set(EXPERIMENTAL_SECTION, { example_flag: false }); const state = flags.explain('example_flag'); @@ -128,14 +129,41 @@ describe('FlagService', () => { expect(state?.configValue).toBe(false); }); - it('lets the master env switch force every flag on', async () => { + it('lets per-feature env force a flag off against config and the master env', async () => { + const { config, flags } = makeFlags({ + KIMI_CODE_EXPERIMENTAL_EXAMPLE_FLAG: 'false', + [MASTER_ENV]: '1', + }); + await config.set(EXPERIMENTAL_SECTION, { example_flag: true }); + const state = flags.explain('example_flag'); + expect(state?.enabled).toBe(false); + expect(state?.source).toBe('env'); + expect(state?.configValue).toBe(true); + }); + + it('lets config override the master env', async () => { const { config, flags } = makeFlags({ [MASTER_ENV]: '1' }); await config.set(EXPERIMENTAL_SECTION, { example_flag: false }); const state = flags.explain('example_flag'); + expect(state?.enabled).toBe(false); + expect(state?.source).toBe('config'); + expect(state?.configValue).toBe(false); + }); + + it('lets the master env switch turn flags on when nothing else is set', () => { + const { flags } = makeFlags({ [MASTER_ENV]: '1' }); + const state = flags.explain('example_flag'); expect(state?.enabled).toBe(true); expect(state?.source).toBe('master-env'); }); + it('treats a falsy master env as unset', () => { + const { flags } = makeFlags({ [MASTER_ENV]: '0' }); + const state = flags.explain('example_flag'); + expect(state?.enabled).toBe(true); + expect(state?.source).toBe('default'); + }); + it('refreshes overrides when the experimental config section changes', async () => { const { config, flags } = makeFlags(); expect(flags.enabled('example_flag')).toBe(true); diff --git a/packages/agent-core-v2/test/features/tower/towerFeature.test.ts b/packages/agent-core-v2/test/features/tower/towerFeature.test.ts index 8a82382cfc2..8d540cbc854 100644 --- a/packages/agent-core-v2/test/features/tower/towerFeature.test.ts +++ b/packages/agent-core-v2/test/features/tower/towerFeature.test.ts @@ -192,16 +192,25 @@ describe('tower flag — resolution', () => { }); }); - it('lets the master env override the dedicated env and config', async () => { - const { config, flags } = makeFlags({ - [TOWER_FLAG_ENV]: 'false', - [MASTER_ENV]: 'true', - }); - await config.set(EXPERIMENTAL_SECTION, { [TOWER_FLAG_ID]: false }); - expect(flags.explain(TOWER_FLAG_ID)).toMatchObject({ + it('lets the master env turn the flag on only when env and config are unset', async () => { + const masterOnly = makeFlags({ [MASTER_ENV]: 'true' }); + expect(masterOnly.flags.explain(TOWER_FLAG_ID)).toMatchObject({ enabled: true, source: 'master-env', + }); + + const configured = makeFlags({ [MASTER_ENV]: 'true' }); + await configured.config.set(EXPERIMENTAL_SECTION, { [TOWER_FLAG_ID]: false }); + expect(configured.flags.explain(TOWER_FLAG_ID)).toMatchObject({ + enabled: false, + source: 'config', configValue: false, }); + + const overridden = makeFlags({ [TOWER_FLAG_ENV]: 'false', [MASTER_ENV]: 'true' }); + expect(overridden.flags.explain(TOWER_FLAG_ID)).toMatchObject({ + enabled: false, + source: 'env', + }); }); });