Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/manual-permission-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix `kimi web` honoring configured permission rules in manual mode.
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
/**
* `permissionRules` domain (L3) — `IAgentPermissionRulesService` implementation.
*
* Holds the agent's permission rules and deduped session-approval patterns in the
* `wire` `PermissionRulesModel`, mutating it only through the `permission.rules.add`
* / `permission.record_approval_result` Ops (`wire.dispatch(...)`) and reading it
* through `wire.getModel`. `wire.replay` rebuilds the model silently and
* consumers read the getters instead. Bound at Agent scope.
* Exposes the effective permission rules by composing the user-configured
* `[permission]` rules from `config` with the agent's live rules in the `wire`
* `PermissionRulesModel`. Mutates agent state only through the
* `permission.rules.add` / `permission.record_approval_result` Ops
* (`wire.dispatch(...)`); `wire.replay` rebuilds the model silently. Bound at
* Agent scope.
*/

import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope';

import { IConfigService } from '#/app/config/config';
import { IWireService } from '#/wire/wire';
import { PERMISSION_SECTION, type PermissionConfig } from './configSection';
import {
IAgentPermissionRulesService,
type PermissionApprovalResultRecord,
Expand All @@ -25,10 +28,15 @@ import {
export class AgentPermissionRulesService implements IAgentPermissionRulesService {
declare readonly _serviceBrand: undefined;

constructor(@IWireService private readonly wire: IWireService) {}
constructor(
@IConfigService private readonly config: IConfigService,
@IWireService private readonly wire: IWireService,
) {}

get rules(): readonly PermissionRule[] {
return [...this.wire.getModel(PermissionRulesModel).rules];
const configuredRules =
this.config.get<PermissionConfig | undefined>(PERMISSION_SECTION)?.rules ?? [];
return [...configuredRules, ...this.wire.getModel(PermissionRulesModel).rules];
}

get sessionApprovalRulePatterns(): readonly string[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,22 @@ describe('AgentPermissionPolicyService chain', () => {
});
});

it('approves a configured MCP tool before the manual-mode fallback', async () => {
rules.push({
decision: 'allow',
scope: 'user',
pattern: 'mcp__example__read_data',
});

await expect(evaluate({
toolName: 'mcp__example__read_data',
args: {},
})).resolves.toMatchObject({
policyName: 'user-configured-allow',
result: { kind: 'approve' },
});
});

it('reuses approve-for-session before matching ask rules', async () => {
rules.push({
decision: 'ask',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TestInstantiationService } from '#/_base/di/test';
import { IAgentPermissionRulesService, type PermissionApprovalResultRecord, type PermissionRule } from '#/agent/permissionRules/permissionRules';
import { AgentPermissionRulesService } from '#/agent/permissionRules/permissionRulesService';
import { PermissionRulesModel } from '#/agent/permissionRules/permissionRulesOps';
import { IConfigService } from '#/app/config/config';
import { AppendLogStore } from '#/persistence/backends/node-fs/appendLogStore';
import { InMemoryStorageService } from '#/persistence/backends/memory/inMemoryStorageService';
import { IAppendLogStore } from '#/persistence/interface/appendLogStore';
Expand Down Expand Up @@ -36,10 +37,15 @@ let disposables: DisposableStore;
let ix: TestInstantiationService;
let log: IAppendLogStore;
let svc: IAgentPermissionRulesService;
let configuredRules: PermissionRule[];

beforeEach(() => {
disposables = new DisposableStore();
ix = disposables.add(new TestInstantiationService());
configuredRules = [];
ix.stub(IConfigService, {
get: (() => ({ rules: configuredRules })) as IConfigService['get'],
} as unknown as IConfigService);
ix.stub(IFileSystemStorageService, new InMemoryStorageService());
ix.set(IAppendLogStore, new SyncDescriptor(AppendLogStore));
ix.set(IAgentPermissionRulesService, new SyncDescriptor(AgentPermissionRulesService));
Expand All @@ -60,6 +66,13 @@ async function readRecords(): Promise<WireRecord[]> {
}

describe('AgentPermissionRulesService (wire-backed)', () => {
it('exposes configured rules before agent runtime rules', () => {
configuredRules.push(denyRule);
svc.addRules([allowRule]);

expect(svc.rules).toEqual([denyRule, allowRule]);
});

it('addRules appends rules and exposes the accumulated rules', () => {
expect(svc.rules).toEqual([]);

Expand Down