feat: 适配 kimi-code 0.40.1 —— flag 清单/优先级语义/危险命令守卫

- flag 清单 9→10:新增 file_history、search_worker;secondary-model
  0.40.1 起默认开启(defaultEnabled)
- env 探测补齐为 12 项元组(10 flag + master + legacy),修正此前
  tower/subagent_fork/wait_for/auto_session_title 四项漏探
- 优先级语义跟随上游 flagService:单 flag env > [experimental] 显式值
  > master env(仅强制开)> 默认。master env 不再锁定 UI;setExperimentalFlag
  增加 explicitFalse 路径,关闭默认开/master 强开的 flag 时写字面 false
- 权限设置新增 dangerous_command_guard 开关(默认开,写 [permission]
  蛇形键;env KIMI_CODE_DANGEROUS_COMMAND_GUARD 运行时覆盖 config)
- 新增 11 个测试(注册表/写语义/守卫读写)
This commit is contained in:
KimiSwitch Dev committed 2026-09-03 10:10:52 +08:00
1 parent 6a7c8f5393
commit 86a128efea
10 files changed
+339 -47

No files matched your search

+19
View File
@@ -157,6 +157,25 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
</Card>
<Card title={t("permissionRules")}>
{/* kimi-code 0.40.1 `[permission] dangerous_command_guard`. The env
var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false")
outranks this config at runtime; no env-lock UI here (deliberate
minimal scope). Absent key = upstream default on. */}
<Checkbox
label={t("permissionDangerousGuard")}
checked={settings.permission?.dangerous_command_guard ?? true}
onChange={(checked) =>
update({
permission: {
...settings.permission,
dangerous_command_guard: checked,
},
})
}
/>
<p className="text-xs text-content-muted">
{t("permissionDangerousGuardDesc")}
</p>
<div className="space-y-2">
{(settings.permission?.rules ?? []).map((rule, idx) => (
<div key={idx} className="flex items-center gap-2">
+48 -18
View File
@@ -57,6 +57,8 @@ const FLAG_LABELS: Record<string, { name: TranslationKey; desc: TranslationKey }
desc: "flagAutoSessionTitleDesc",
},
"remote-control": { name: "flagRemoteControl", desc: "flagRemoteControlDesc" },
search_worker: { name: "flagSearchWorker", desc: "flagSearchWorkerDesc" },
file_history: { name: "flagFileHistory", desc: "flagFileHistoryDesc" },
};
/** Validation-error i18n key per engine rule, for the pre-write self-check. */
@@ -187,13 +189,18 @@ export function SubagentSettingsPage({
const secondaryFlag = EXPERIMENTAL_FLAGS[0]; // "secondary-model"
const otherFlags = EXPERIMENTAL_FLAGS.slice(1);
/** Effective state of the secondary-model flag (env overrides config). */
const secondaryEnabled = masterOn
? true
: isFlagLockedByEnv(env, secondaryFlag)
? forcedEnvValue(env, secondaryFlag)
: flags["secondary-model"] === true;
const secondaryLocked = masterOn || isFlagLockedByEnv(env, secondaryFlag);
/** Effective state of the secondary-model flag. Mirrors the kimi-code
* 0.40.1 priority: single-flag env > explicit [experimental] value >
* master env (force-on only) > upstream default. The master env no
* longer locks the toggle — only the flag's own env var does. */
const secondaryEnabled = isFlagLockedByEnv(env, secondaryFlag)
? forcedEnvValue(env, secondaryFlag)
: isExperimentalFlagSet(rawOther, secondaryFlag.id)
? flags["secondary-model"] === true
: masterOn
? true
: (secondaryFlag.defaultEnabled ?? false);
const secondaryLocked = isFlagLockedByEnv(env, secondaryFlag);
const aliasList = Object.keys(models).sort();
@@ -302,17 +309,24 @@ export function SubagentSettingsPage({
const renderFlagRow = (flag: ExperimentalFlagDef) => {
const labels = FLAG_LABELS[flag.id];
const locked = masterOn || isFlagLockedByEnv(env, flag);
// An explicitly written flag (true or false) wins; an absent flag falls
// back to the upstream default (`defaultEnabled`).
// Only the flag's own env var locks the toggle. The master env force-ons
// undefined flags but an explicit config entry outranks it, so the user
// can still flip values here while it is set.
const locked = isFlagLockedByEnv(env, flag);
// Resolution order mirrors kimi-code 0.40.1 flagService:
// single-flag env > explicit [experimental] value (true *or* false) >
// master env (on-only) > upstream default (`defaultEnabled`).
const explicitlySet = isExperimentalFlagSet(rawOther, flag.id);
const on = masterOn
? true
: isFlagLockedByEnv(env, flag)
? forcedEnvValue(env, flag)
: explicitlySet
? flags[flag.id] === true
const on = locked
? forcedEnvValue(env, flag)
: explicitlySet
? flags[flag.id] === true
: masterOn
? true
: (flag.defaultEnabled ?? false);
// Turning the flag off only sticks via an explicit `false` when the
// fallback (master env or the upstream default) would otherwise keep it on.
const explicitFalse = masterOn || flag.defaultEnabled === true;
return (
<div key={flag.id} className="flex items-center gap-3">
<div className="flex-1 min-w-0">
@@ -339,7 +353,9 @@ export function SubagentSettingsPage({
disabled={locked}
ariaLabel={t(labels.name)}
onChange={(checked) =>
onChange(setExperimentalFlag(rawOther, flag.id, checked))
onChange(
setExperimentalFlag(rawOther, flag.id, checked, { explicitFalse })
)
}
/>
</div>
@@ -560,6 +576,16 @@ export function SubagentSettingsPage({
<div className="border-t border-border" />
<div className="flex items-center gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 text-sm text-content-primary">
{t(FLAG_LABELS[secondaryFlag.id].name)}
{!secondaryLocked &&
!isExperimentalFlagSet(rawOther, secondaryFlag.id) &&
secondaryFlag.defaultEnabled && (
<span className="shrink-0 text-xs text-blue-600 dark:text-blue-400 border border-blue-500/30 rounded px-1">
{t("flagDefaultOn")}
</span>
)}
</div>
<div className="text-xs text-content-muted">
{t(FLAG_LABELS[secondaryFlag.id].desc)}
<code className="ml-2">{secondaryFlag.envVar}</code>
@@ -575,7 +601,11 @@ export function SubagentSettingsPage({
disabled={secondaryLocked}
ariaLabel={t(FLAG_LABELS[secondaryFlag.id].name)}
onChange={(checked) =>
onChange(setExperimentalFlag(rawOther, secondaryFlag.id, checked))
onChange(
setExperimentalFlag(rawOther, secondaryFlag.id, checked, {
explicitFalse: masterOn || secondaryFlag.defaultEnabled === true,
})
)
}
/>
</div>
+10 -1
View File
@@ -151,6 +151,9 @@ export const enTranslations: Record<TranslationKey, string> = {
permissionAllow: "Allow",
permissionDeny: "Deny",
permissionAsk: "Ask",
permissionDangerousGuard: "Dangerous Command Guard",
permissionDangerousGuardDesc:
"When on (default): Auto mode refuses dangerous commands (rm -rf, shutdown, dd of=, ...) and other modes always prompt; turning it off restores the previous behavior. Env KIMI_CODE_DANGEROUS_COMMAND_GUARD overrides this config",
addRule: "+ Add rule",
addCommonRules: "Add common rules",
hooks: "Lifecycle Hooks",
@@ -477,9 +480,15 @@ export const enTranslations: Record<TranslationKey, string> = {
flagAutoSessionTitleDesc: "Generate session titles automatically via the managed chat_title tool",
flagRemoteControl: "Remote Control",
flagRemoteControlDesc: "Enable the experimental remote control feature",
flagSearchWorker: "Search Worker",
flagSearchWorkerDesc: "Run the kap-server global search backend in a background worker host",
flagFileHistory: "File History",
flagFileHistoryDesc:
"Snapshot edited files before/after each turn (last 5 turns across 30 sessions, stored in the session data dir) for real whole-file diffs",
flagDefaultOn: "On by default",
lockedByEnv: "Locked by env var",
masterEnvOnHint: "KIMI_CODE_EXPERIMENTAL_FLAG is set: all experimental features are forced on by the env var",
masterEnvOnHint:
"KIMI_CODE_EXPERIMENTAL_FLAG is set: flags without an explicit [experimental] entry are forced on; explicit entries in config.toml still win",
secondaryModelSection: "Subagent Model (Secondary Model)",
secondaryModelDisabledHint:
"The experimental flag \"Subagent Secondary Model\" is off and this config will not take effect — enable the toggle above first",
+10 -1
View File
@@ -149,6 +149,9 @@ export const zhTranslations = {
permissionAllow: "允许",
permissionDeny: "拒绝",
permissionAsk: "询问",
permissionDangerousGuard: "危险命令防护",
permissionDangerousGuardDesc:
"开启(默认)时:Auto 模式直接拒绝危险命令(rm -rf、shutdown、dd of= 等),其它模式强制询问;关闭后回归旧行为。环境变量 KIMI_CODE_DANGEROUS_COMMAND_GUARD 会覆盖本配置",
addRule: "+ 添加规则",
addCommonRules: "添加常用规则",
hooks: "生命周期钩子",
@@ -472,9 +475,15 @@ export const zhTranslations = {
flagAutoSessionTitleDesc: "通过托管 chat_title 工具自动生成会话标题",
flagRemoteControl: "远程控制",
flagRemoteControlDesc: "启用实验性远程控制功能",
flagSearchWorker: "搜索 Worker",
flagSearchWorkerDesc: "kap-server 全局搜索的后台 worker 宿主服务",
flagFileHistory: "文件历史",
flagFileHistoryDesc:
"记录每轮编辑文件的前后快照(最近 30 个会话×最近 5 轮,存于会话数据目录),用于真实的整文件 diff",
flagDefaultOn: "默认开启",
lockedByEnv: "被环境变量锁定",
masterEnvOnHint: "KIMI_CODE_EXPERIMENTAL_FLAG 已设置:全部实验功能被环境变量强制开启",
masterEnvOnHint:
"KIMI_CODE_EXPERIMENTAL_FLAG 已设置:未显式配置的实验项将被强制开启;config.toml 中的显式配置优先生效",
secondaryModelSection: "子代理模型(次主力模型)",
secondaryModelDisabledHint:
"实验开关「子代理次主力模型」未启用,本配置不会生效——请先在上方打开对应开关",
+72
View File
@@ -131,4 +131,76 @@ describe("loop_control — v1/v2 key handling", () => {
expect(s.loop_control?.max_attempts_per_step).toBe(4);
expect(s.loop_control?.max_retries_per_step).toBe(4);
});
});
// ---------------------------------------------------------------------------
// [permission] dangerous_command_guard (kimi-code 0.40.1) — absent key means
// upstream default ON; an explicit false must survive even with no rules
// ---------------------------------------------------------------------------
describe("permission.dangerous_command_guard", () => {
it("reads as undefined when the key is absent (default on)", () => {
expect(
getAgentSettings({}).permission?.dangerous_command_guard
).toBeUndefined();
expect(
getAgentSettings({ permission: { rules: [] } }).permission
?.dangerous_command_guard
).toBeUndefined();
});
it("reads an explicit true / false", () => {
expect(
getAgentSettings({ permission: { dangerous_command_guard: false } })
.permission?.dangerous_command_guard
).toBe(false);
expect(
getAgentSettings({ permission: { dangerous_command_guard: true } })
.permission?.dangerous_command_guard
).toBe(true);
});
it("writes guard=false even with no rules (section kept)", () => {
const next = setAgentSettings(
{},
{ permission: { rules: [], dangerous_command_guard: false } }
) as { permission?: Record<string, unknown> };
expect(next.permission).toEqual({ dangerous_command_guard: false });
});
it("writes guard together with rules", () => {
const rules = [{ decision: "allow" as const, pattern: "Read" }];
const next = setAgentSettings(
{},
{ permission: { rules, dangerous_command_guard: true } }
) as { permission?: Record<string, unknown> };
expect(next.permission).toEqual({
rules,
dangerous_command_guard: true,
});
});
it("a rules-only update carries the existing guard through", () => {
const raw = { permission: { dangerous_command_guard: false } };
const next = setAgentSettings(raw, {
permission: { rules: [{ decision: "deny" as const, pattern: "Bash" }] },
}) as { permission?: Record<string, unknown> };
expect(next.permission?.dangerous_command_guard).toBe(false);
expect(next.permission?.rules).toHaveLength(1);
});
it("omits an empty rules array; keeps an explicit guard on plain saves", () => {
const raw = { permission: { dangerous_command_guard: true } };
const off = setAgentSettings(raw, {
permission: { rules: [], dangerous_command_guard: false },
}) as { permission?: Record<string, unknown> };
expect(off.permission).toEqual({ dangerous_command_guard: false });
expect(off.permission).not.toHaveProperty("rules");
// An explicit true is materialized on plain saves (harmless, and it
// documents the state the UI toggle shows).
const on = setAgentSettings(raw, {}) as {
permission?: Record<string, unknown>;
};
expect(on.permission).toEqual({ dangerous_command_guard: true });
});
});
+36 -8
View File
@@ -65,6 +65,10 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
if (thinking.effort === "max") {
thinking.effort = "high";
}
const sectionPermission = getSection<AgentSettings["permission"]>(
rawOther,
"permission"
);
return {
thinking,
loop_control: loop,
@@ -73,9 +77,15 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
...getSection<AgentSettings["background"]>(rawOther, "background"),
},
permission: {
rules:
getSection<AgentSettings["permission"]>(rawOther, "permission")?.rules ??
[],
rules: sectionPermission?.rules ?? [],
// `dangerous_command_guard` stays undefined unless the config carries
// an explicit boolean — upstream defaults it to ON (kimi-code 0.40.1),
// so an absent key means "on". The env var
// KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false" only)
// outranks this config value at kimi-code runtime.
...(typeof sectionPermission?.dangerous_command_guard === "boolean"
? { dangerous_command_guard: sectionPermission.dangerous_command_guard }
: {}),
},
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
};
@@ -88,13 +98,20 @@ export function setAgentSettings(
): unknown {
const root = { ...asRecord(rawOther) };
const current = getAgentSettings(rawOther);
// Explicit guard value wins over the carried-over one; undefined keeps the
// key absent (upstream default = on).
const guard =
patch.permission?.dangerous_command_guard ??
current.permission?.dangerous_command_guard;
const permission: NonNullable<AgentSettings["permission"]> = {
rules: patch.permission?.rules ?? current.permission?.rules ?? [],
...(typeof guard === "boolean" ? { dangerous_command_guard: guard } : {}),
};
const next: AgentSettings = {
thinking: { ...current.thinking, ...patch.thinking },
loop_control: { ...current.loop_control, ...patch.loop_control },
background: { ...current.background, ...patch.background },
permission: {
rules: patch.permission?.rules ?? current.permission?.rules ?? [],
},
permission,
hooks: patch.hooks ?? current.hooks ?? [],
};
@@ -115,8 +132,19 @@ export function setAgentSettings(
if (next.thinking) root.thinking = next.thinking;
if (next.loop_control) root.loop_control = next.loop_control;
if (next.background) root.background = next.background;
if (next.permission?.rules && next.permission.rules.length > 0) {
root.permission = next.permission;
// Keep `[permission]` when it carries rules *or* an explicit
// dangerous_command_guard — dropping the section would silently lose a
// guard=false write (absent key = upstream default on). An empty rules
// array is omitted rather than written as `rules = []`.
const hasRules = !!permission.rules && permission.rules.length > 0;
const hasGuard = typeof permission.dangerous_command_guard === "boolean";
if (hasRules || hasGuard) {
root.permission = {
...(hasRules ? { rules: permission.rules } : {}),
...(hasGuard
? { dangerous_command_guard: permission.dangerous_command_guard }
: {}),
};
} else {
delete root.permission;
}
+77
View File
@@ -1,8 +1,12 @@
import { describe, expect, it } from "vitest";
import {
EXPERIMENTAL_FLAGS,
getExperimentalFlags,
getSecondaryModel,
getSubagentModelPool,
isExperimentalFlagSet,
removeSubagentPoolEntry,
setExperimentalFlag,
setSecondaryModelOnly,
setSubagentDefault,
setSubagentForce,
@@ -449,3 +453,76 @@ describe("validateSubagentPool", () => {
expect(errors).toContainEqual({ key: "forceExcludesModels" });
});
});
// ---------------------------------------------------------------------------
// [experimental] — flag registry (kimi-code 0.40.1) + write semantics
// ---------------------------------------------------------------------------
describe("experimental flag registry", () => {
it("mirrors the 0.40.1 v2 registry (10 flags incl. file_history)", () => {
expect(EXPERIMENTAL_FLAGS.map((f) => f.id)).toEqual([
"secondary-model",
"tool-select",
"persistence_minidb_readmodel",
"tower",
"subagent_fork",
"wait_for",
"auto_session_title",
"remote-control",
"search_worker",
"file_history",
]);
const fh = EXPERIMENTAL_FLAGS.find((f) => f.id === "file_history");
expect(fh?.envVar).toBe("KIMI_CODE_EXPERIMENTAL_FILE_HISTORY");
expect(fh?.defaultEnabled).toBeUndefined();
});
it("secondary-model is on by default since 0.40.1", () => {
expect(
EXPERIMENTAL_FLAGS.find((f) => f.id === "secondary-model")?.defaultEnabled
).toBe(true);
});
});
describe("setExperimentalFlag", () => {
const rawExp = (section: Record<string, unknown>) => ({
experimental: section,
});
it("enabling writes true; disabling a default-off flag deletes the key", () => {
const on = setExperimentalFlag({}, "tool-select", true) as {
experimental: Record<string, unknown>;
};
expect(on.experimental["tool-select"]).toBe(true);
const off = setExperimentalFlag(on, "tool-select", false) as Record<
string,
unknown
>;
expect(off).not.toHaveProperty("experimental"); // empty section dropped
});
it("disabling a default-on flag with explicitFalse writes a literal false", () => {
const next = setExperimentalFlag(
rawExp({ wait_for: true }),
"wait_for",
false,
{ explicitFalse: true }
) as { experimental: Record<string, unknown> };
expect(next.experimental.wait_for).toBe(false);
expect(isExperimentalFlagSet(next, "wait_for")).toBe(true);
expect(getExperimentalFlags(next).wait_for).toBe(false);
});
it("an explicit false coexists with other flags and keeps the section", () => {
const next = setExperimentalFlag(
rawExp({ "secondary-model": false }),
"tower",
true,
{ explicitFalse: true }
) as { experimental: Record<string, unknown> };
expect(next.experimental).toEqual({
"secondary-model": false,
tower: true,
});
});
});
+30 -6
View File
@@ -36,10 +36,16 @@ export interface ExperimentalFlagDef {
/** Known experimental flags — mirrors the kimi-code v2 flag registry
* (the per-feature flag.ts files under packages/agent-core-v2/src:
* secondary-model, tool-select, persistence_minidb_readmodel, tower,
* subagent_fork, wait_for, auto_session_title, remote-control). `acp-v2` was
* removed upstream and is dropped here. */
* subagent_fork, wait_for, auto_session_title, remote-control,
* search_worker, file_history). `acp-v2` was removed upstream and is
* dropped here. Keep the env-var list in sync with
* `get_experimental_env_status` in src-tauri/src/commands.rs. */
export const EXPERIMENTAL_FLAGS: ExperimentalFlagDef[] = [
{ id: "secondary-model", envVar: "KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL" },
{
id: "secondary-model",
envVar: "KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL",
defaultEnabled: true, // on by default since kimi-code 0.40.1
},
{ id: "tool-select", envVar: "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT" },
{
id: "persistence_minidb_readmodel",
@@ -58,11 +64,21 @@ export const EXPERIMENTAL_FLAGS: ExperimentalFlagDef[] = [
envVar: "KIMI_CODE_EXPERIMENTAL_AUTO_SESSION_TITLE",
},
{ id: "remote-control", envVar: "KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL" },
{
id: "search_worker",
envVar: "KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER",
defaultEnabled: true,
},
{ id: "file_history", envVar: "KIMI_CODE_EXPERIMENTAL_FILE_HISTORY" },
];
export const EXPERIMENTAL_MASTER_ENV = "KIMI_CODE_EXPERIMENTAL_FLAG";
/** Whether the master env var is set to a truthy value (locks every flag on). */
/** Whether the master env var is set to a truthy value. Since kimi-code
* 0.40.1 it only force-ONs flags that have *no* explicit `[experimental]`
* entry (an explicit config value — true or false — outranks it, and the
* master env can never force a flag off). It locks no UI toggles; only a
* flag's own single-feature env var does. */
export function isMasterEnvOn(env: ExperimentalEnvStatus | null): boolean {
return isTruthyEnv(env?.[EXPERIMENTAL_MASTER_ENV]);
}
@@ -105,16 +121,24 @@ export function isExperimentalFlagSet(rawOther: unknown, id: string): boolean {
);
}
/** Set one flag in `[experimental]`. Removing all flags drops the section. */
/** Set one flag in `[experimental]`. Removing all flags drops the section.
* Pass `explicitFalse` when deleting the key would NOT express "off" —
* i.e. the flag is on by upstream default (`defaultEnabled`) or the master
* env force-ons undefined flags — and write a literal `false` instead,
* which outranks both under the kimi-code 0.40.1 priority
* (single env > explicit config > master env > default). */
export function setExperimentalFlag(
rawOther: unknown,
id: string,
enabled: boolean
enabled: boolean,
opts?: { explicitFalse?: boolean }
): unknown {
const root = { ...asRecord(rawOther) };
const flags = { ...getExperimentalFlags(rawOther) };
if (enabled) {
flags[id] = true;
} else if (opts?.explicitFalse) {
flags[id] = false;
} else {
delete flags[id];
}
+9 -1
View File
@@ -149,7 +149,15 @@ export interface AgentSettings {
thinking?: ThinkingConfig;
loop_control?: LoopControlConfig;
background?: BackgroundConfig;
permission?: { rules?: PermissionRule[] };
permission?: {
rules?: PermissionRule[];
/** kimi-code 0.40.1 `[permission]` key. Default true when the key is
* absent: Auto mode refuses dangerous commands (rm -rf, shutdown,
* dd of=, …) and other modes always prompt; false restores the
* previous behavior. Env KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal
* "true"/"false" only) outranks this config. */
dangerous_command_guard?: boolean;
};
hooks?: Hook[];
}