diff --git a/.changeset/manual-permission-rules.md b/.changeset/manual-permission-rules.md new file mode 100644 index 0000000000..b4287b01c8 --- /dev/null +++ b/.changeset/manual-permission-rules.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix `kimi web` honoring configured permission rules in manual mode. diff --git a/packages/agent-core-v2/src/agent/permissionRules/permissionRulesService.ts b/packages/agent-core-v2/src/agent/permissionRules/permissionRulesService.ts index 7db0f2e179..993945c3d1 100644 --- a/packages/agent-core-v2/src/agent/permissionRules/permissionRulesService.ts +++ b/packages/agent-core-v2/src/agent/permissionRules/permissionRulesService.ts @@ -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, @@ -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(PERMISSION_SECTION)?.rules ?? []; + return [...configuredRules, ...this.wire.getModel(PermissionRulesModel).rules]; } get sessionApprovalRulePatterns(): readonly string[] { diff --git a/packages/agent-core-v2/test/agent/permissionPolicy/permissionPolicyService.test.ts b/packages/agent-core-v2/test/agent/permissionPolicy/permissionPolicyService.test.ts index 47e133f418..323f3e1aea 100644 --- a/packages/agent-core-v2/test/agent/permissionPolicy/permissionPolicyService.test.ts +++ b/packages/agent-core-v2/test/agent/permissionPolicy/permissionPolicyService.test.ts @@ -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', diff --git a/packages/agent-core-v2/test/agent/permissionRules/permissionRules.test.ts b/packages/agent-core-v2/test/agent/permissionRules/permissionRules.test.ts index 517edaa7c7..596d4f484e 100644 --- a/packages/agent-core-v2/test/agent/permissionRules/permissionRules.test.ts +++ b/packages/agent-core-v2/test/agent/permissionRules/permissionRules.test.ts @@ -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'; @@ -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)); @@ -60,6 +66,13 @@ async function readRecords(): Promise { } 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([]);