import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, normalize } from 'pathe'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { Emitter, Event } from '#/_base/event'; import { InstantiationService } from '#/_base/di/instantiationService'; import { ServiceCollection } from '#/_base/di/serviceCollection'; import { ConfigTarget, IConfigService } from '#/app/config/config'; import { TOOLS_SECTION } from '#/agent/toolPolicy/configSection'; import { DEFAULT_AGENT_PROFILE_NAME, normalizeAgentProfile, } from '#/app/agentProfileCatalog/agentProfileCatalog'; import { BuiltinAgentProfileLoaderService } from '#/app/agentProfileCatalog/builtinAgentProfileLoaderService'; import { registerAgentProfile } from '#/app/agentProfileCatalog/contribution'; import type { ToolCall } from '#human/llm/message'; import { IAgentProfileService, type ResolvedAgentProfile } from '#/agent/profile/profile'; import type { WatchChange } from '#human/utils/watch'; import { IAgentAgentsMdReminderService } from '#/agent/agentsMdReminder/agentsMdReminder'; import { IAgentToolPolicyService } from '#/agent/toolPolicy/toolPolicy'; import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor'; import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; import { SELECT_TOOLS_TOOL_NAME } from '#/agent/toolSelect/toolSelect'; import { IAtomicDocumentStore, type IAtomicDocumentStore as AtomicDocumentStore } from '#/persistence/interface/atomicDocumentStore'; import { ISessionAgentProfileCatalog } from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog'; import { ISessionContext } from '#/session/sessionContext/sessionContext'; import { ISessionInstructionsProvider } from '#/session/sessionInstructions/instructionsProvider'; import { ISessionSkillCatalog } from '#/features/skill/session/skillCatalog'; import { ISessionToolPolicy } from '#/session/sessionToolPolicy/sessionToolPolicy'; import { ISessionToolPolicyGate } from '#/session/sessionToolPolicyGate/sessionToolPolicyGate'; import { IWireService } from '#/wire/wire'; import type { ExecutableTool, ToolExecution, ToolResult, ToolSource } from '#/tool/toolContract'; import { IAgentIdentity } from '#/app/agentIdentity/agentIdentity'; import { deferredAgentIdentityStub } from '../../app/agentIdentity/stubs'; import { InMemoryWireRecordPersistence, agentService, appService, createTestAgent, hostEnvironmentServices, sessionService, type TestAgentContext, } from '../../harness'; const MOCK_MODEL = 'mock-model'; function profileServices(ctx: TestAgentContext): { profile: IAgentProfileService; toolPolicy: IAgentToolPolicyService; } { return { profile: ctx.get(IAgentProfileService), toolPolicy: ctx.get(IAgentToolPolicyService), }; } function createAtomicDocumentStore(): AtomicDocumentStore { const documents = new Map(); const documentKey = (scope: string, key: string): string => `${scope}/${key}`; return { _serviceBrand: undefined, get: async (scope: string, key: string) => documents.get(documentKey(scope, key)) as T | undefined, set: async (scope: string, key: string, value: T) => { documents.set(documentKey(scope, key), structuredClone(value)); }, delete: async (scope: string, key: string) => { documents.delete(documentKey(scope, key)); }, list: async (scope: string, prefix = '') => [...documents.keys()] .filter((key) => key.startsWith(`${scope}/${prefix}`)) .map((key) => key.slice(scope.length + 1)), acquire: () => ({ dispose: () => {} }), }; } describe('AgentProfileService.bind', () => { let ctx: TestAgentContext; let homeDir: string; beforeAll(() => { registerAgentProfile({ name: 'delegates-explore', subagents: ['explore'], systemPrompt: () => 'delegate test', }); }); beforeEach(async () => { homeDir = await mkdtemp(join(tmpdir(), 'kimi-bind-home-')); }); afterEach(async () => { await ctx?.dispose(); await rm(homeDir, { recursive: true, force: true }); }); function buildContext(): { ctx: TestAgentContext; profile: IAgentProfileService } { ctx = createTestAgent(hostEnvironmentServices(homeDir)); return { ctx, profile: ctx.get(IAgentProfileService) }; } it('binds a profile + model atomically and becomes runnable', async () => { const { profile: svc } = buildContext(); const container = new InstantiationService(new ServiceCollection(), true); const catalog = new BuiltinAgentProfileLoaderService(container); expect(catalog.get(DEFAULT_AGENT_PROFILE_NAME)).toBeDefined(); catalog.dispose(); container.dispose(); expect(svc.isRunnable()).toBe(false); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); expect(svc.data().profileName).toBe(DEFAULT_AGENT_PROFILE_NAME); expect(svc.data().modelAlias).toBe(MOCK_MODEL); expect(svc.isRunnable()).toBe(true); expect(svc.getActiveToolNames()?.length).toBeGreaterThan(0); expect(svc.getSystemPrompt()).toContain('Kimi Code CLI'); }); it('waits for the identity freeze instead of racing it', async () => { const deferred = deferredAgentIdentityStub(); ctx = createTestAgent( appService(IAgentIdentity, deferred.identity), hostEnvironmentServices(homeDir), ); const svc = ctx.get(IAgentProfileService); const bound = svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); setTimeout(() => deferred.freeze(), 20); await bound; expect(svc.data().modelAlias).toBe(MOCK_MODEL); expect(svc.isRunnable()).toBe(true); }); it('binds an environment disclosure snapshot with only the session cwd', async () => { ctx = createTestAgent(hostEnvironmentServices(homeDir)); const svc = ctx.get(IAgentProfileService); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); expect(svc.getSystemPrompt()).not.toContain('2026-07-29'); expect(svc.data().environmentDisclosure).toEqual({ cwd: ctx.get(ISessionContext).cwd }); }); it('persists the complete binding in one journal record', async () => { const persistence = new InMemoryWireRecordPersistence(); ctx = createTestAgent( { persistence, initialConfig: { thinking: { enabled: true, effort: 'low' }, }, }, hostEnvironmentServices(homeDir), ); ctx.configure({ modelCapabilities: { image_in: false, video_in: false, audio_in: false, thinking: true, tool_use: true, max_context_tokens: 1_000_000, }, }); const svc = ctx.get(IAgentProfileService); await ctx.get(IWireService).flush(); const start = persistence.records.length; await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL, thinking: 'low', }); await ctx.get(IWireService).flush(); const records = persistence.records.slice(start).filter((record) => record.type === 'profile.bind'); expect(records).toHaveLength(1); expect(records[0]).toMatchObject({ type: 'profile.bind', profileName: DEFAULT_AGENT_PROFILE_NAME, modelAlias: MOCK_MODEL, thinkingEffort: 'on', systemPrompt: expect.stringContaining('Kimi Code CLI'), activeToolNames: expect.arrayContaining(['Read', 'Write', 'Bash']), disallowedTools: [], }); }); it('restores the subagent allowlist from the binding record without catalog resolution', async () => { const persistence = new InMemoryWireRecordPersistence(); ctx = createTestAgent({ persistence }, hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile: 'delegates-explore', model: MOCK_MODEL, }); await ctx.get(IWireService).flush(); const bindingRecord = persistence.records.find((record) => record.type === 'profile.bind'); expect(bindingRecord).toMatchObject({ profileName: 'delegates-explore', subagents: ['explore'], }); await ctx.dispose(); const emptyCatalog = { _serviceBrand: undefined, ready: Promise.resolve(), get: () => undefined, getDefault: () => ({ name: DEFAULT_AGENT_PROFILE_NAME, tools: undefined, systemPrompt: () => '', }), list: () => [], load: async () => {}, reload: async () => {}, } as unknown as ISessionAgentProfileCatalog; ctx = createTestAgent( { persistence }, hostEnvironmentServices(homeDir), sessionService(ISessionAgentProfileCatalog, emptyCatalog), ); await ctx.restorePersisted(); expect(ctx.get(IAgentProfileService).data()).toMatchObject({ profileName: 'delegates-explore', subagents: ['explore'], }); expect(ctx.get(IAgentProfileService).data().agentsMdPaths).toEqual( bindingRecord?.['agentsMdPaths'], ); }); it('keeps the system prompt frozen after a default bind when AGENTS.md changes', async () => { const workDir = await mkdtemp(join(tmpdir(), 'kimi-bind-work-')); try { await writeFile(join(workDir, 'AGENTS.md'), 'v1 instructions', 'utf-8'); ctx = createTestAgent(hostEnvironmentServices(homeDir), { cwd: workDir }); const svc = ctx.get(IAgentProfileService); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); const bound = svc.getSystemPrompt(); expect(bound).toContain('v1 instructions'); await writeFile(join(workDir, 'AGENTS.md'), 'v2 instructions', 'utf-8'); expect(svc.getSystemPrompt()).toBe(bound); } finally { await rm(workDir, { recursive: true, force: true }); } }); it('freezes the system prompt when the session instructions change', async () => { const persistence = new InMemoryWireRecordPersistence(); const emitter = new Emitter(); let agentsMd = 'v1 instructions'; ctx = createTestAgent( { persistence }, hostEnvironmentServices(homeDir), sessionService(ISessionInstructionsProvider, { _serviceBrand: undefined, ready: Promise.resolve(), get agentsMd() { return agentsMd; }, agentsMdWarning: undefined, agentsMdPaths: [], onDidChange: emitter.event, } satisfies ISessionInstructionsProvider), ); const svc = ctx.get(IAgentProfileService); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); const before = svc.getSystemPrompt(); expect(before).toContain('v1 instructions'); await ctx.get(IWireService).flush(); const configUpdates = () => persistence.records.filter( (record) => record.type === 'config.update' && 'systemPrompt' in record, ); const configUpdateCount = configUpdates().length; agentsMd = 'v2 instructions'; emitter.fire([{ path: '/repo/AGENTS.md', action: 'modified', kind: 'file' }]); await ctx.get(IWireService).flush(); expect(svc.getSystemPrompt()).toBe(before); expect(configUpdates()).toHaveLength(configUpdateCount); }); it('setModel applies the default profile when none is bound yet', async () => { const { profile: svc } = buildContext(); expect(svc.data().profileName).toBeUndefined(); await svc.setModel(MOCK_MODEL); expect(svc.data().profileName).toBe(DEFAULT_AGENT_PROFILE_NAME); expect(svc.data().modelAlias).toBe(MOCK_MODEL); expect(svc.isRunnable()).toBe(true); }); it('setModel keeps the existing profile when one is already bound', async () => { const { profile: svc } = buildContext(); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); await svc.setModel(MOCK_MODEL); expect(svc.data().profileName).toBe(DEFAULT_AGENT_PROFILE_NAME); }); it('rejects binding a different profile once bound', async () => { const { profile: svc } = buildContext(); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); await expect(svc.bind({ profile: 'coder', model: MOCK_MODEL })).rejects.toThrow( /already bound/, ); expect(svc.data().profileName).toBe(DEFAULT_AGENT_PROFILE_NAME); }); it('rejects an unsupported thinking effort atomically before first bind', async () => { ctx = createTestAgent( { initialConfig: { providers: { kimi: { type: 'kimi', apiKey: 'test-key', baseUrl: 'https://api.example.test/v1' }, }, models: { 'kimi-code/kimi-for-coding': { provider: 'kimi', model: 'kimi-for-coding', maxContextSize: 1_000_000, capabilities: ['thinking'], supportEfforts: ['low', 'high'], }, }, }, }, hostEnvironmentServices(homeDir), ); const svc = ctx.get(IAgentProfileService); await expect( svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: 'kimi-code/kimi-for-coding', thinking: 'ultra', strictThinking: true, }), ).rejects.toThrow(/not supported by model/); expect(svc.data().profileName).toBeUndefined(); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: 'kimi-code/kimi-for-coding' }); expect(svc.data().profileName).toBe(DEFAULT_AGENT_PROFILE_NAME); }); it('clamps an inherited unsupported thinking effort instead of rejecting the bind', async () => { ctx = createTestAgent( { initialConfig: { providers: { kimi: { type: 'kimi', apiKey: 'test-key', baseUrl: 'https://api.example.test/v1' }, }, models: { 'kimi-code/kimi-for-coding': { provider: 'kimi', model: 'kimi-for-coding', maxContextSize: 1_000_000, capabilities: ['thinking'], supportEfforts: ['low', 'high'], }, }, }, }, hostEnvironmentServices(homeDir), ); const svc = ctx.get(IAgentProfileService); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: 'kimi-code/kimi-for-coding', thinking: 'ultra', }); expect(svc.data().profileName).toBe(DEFAULT_AGENT_PROFILE_NAME); expect(svc.data().thinkingLevel).toBe('high'); }); it('keeps the persisted thinking effort on a same-name rebind', async () => { ctx = createTestAgent(hostEnvironmentServices(homeDir)); ctx.configure({ modelCapabilities: { image_in: false, video_in: false, audio_in: false, thinking: true, tool_use: true, max_context_tokens: 1_000_000, }, }); const svc = ctx.get(IAgentProfileService); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL, thinking: 'off' }); expect(svc.data().thinkingLevel).toBe('off'); await svc.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); expect(svc.data().thinkingLevel).toBe('off'); }); }); describe('AgentToolPolicyService tool denylist', () => { beforeAll(() => { registerAgentProfile({ name: 'deny-builtin', disallowedTools: ['Bash'], systemPrompt: () => 'deny test', }); registerAgentProfile({ name: 'deny-over-allow', tools: ['Read', 'Bash'], disallowedTools: ['Bash'], systemPrompt: () => 'deny test', }); registerAgentProfile({ name: 'deny-mcp', disallowedTools: ['mcp__github__*'], systemPrompt: () => 'deny test', }); }); let ctx: TestAgentContext; let homeDir: string; beforeEach(async () => { homeDir = await mkdtemp(join(tmpdir(), 'kimi-deny-home-')); }); afterEach(async () => { await ctx?.dispose(); await rm(homeDir, { recursive: true, force: true }); }); async function bindProfile(name: string): Promise { ctx = createTestAgent(hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile: name, model: MOCK_MODEL }); return ctx.get(IAgentToolPolicyService); } it('blocks a denied builtin tool while others stay active', async () => { const svc = await bindProfile('deny-builtin'); expect(svc.isToolActive('Bash')).toBe(false); expect(svc.isToolActive('Read')).toBe(true); }); it('denylist wins over the allowlist', async () => { const svc = await bindProfile('deny-over-allow'); expect(svc.isToolActive('Bash')).toBe(false); expect(svc.isToolActive('Read')).toBe(true); expect(svc.isToolActive('Write')).toBe(false); }); it('matches denied mcp tools by glob', async () => { const svc = await bindProfile('deny-mcp'); expect(svc.isToolActive('mcp__github__create_pr', 'mcp')).toBe(false); expect(svc.isToolActive('mcp__other__ping', 'mcp')).toBe(true); expect(svc.isToolActive('Read')).toBe(true); }); it('lists available profiles when binding an unknown profile', async () => { ctx = createTestAgent(hostEnvironmentServices(homeDir)); await expect( ctx.get(IAgentProfileService).bind({ profile: 'does-not-exist', model: MOCK_MODEL }), ).rejects.toThrow(/Available profiles: .*agent/); }); it('persists the denylist in the bind records', async () => { const persistence = new InMemoryWireRecordPersistence(); ctx = createTestAgent({ persistence }, hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile: 'deny-builtin', model: MOCK_MODEL }); await ctx.get(IWireService).flush(); const record = persistence.records.find((candidate) => candidate.type === 'profile.bind'); expect(record).toMatchObject({ profileName: 'deny-builtin', disallowedTools: ['Bash'] }); }); it('persists an unrestricted tool policy when the profile has no allowlist', async () => { const persistence = new InMemoryWireRecordPersistence(); ctx = createTestAgent({ persistence }, hostEnvironmentServices(homeDir)); const { profile, toolPolicy } = profileServices(ctx); await profile.bind({ profile: 'deny-builtin', model: MOCK_MODEL }); await ctx.get(IWireService).flush(); expect(persistence.records.find((record) => record.type === 'profile.bind')).toMatchObject({ activeToolNames: undefined, }); expect(toolPolicy.isToolActive('Read')).toBe(true); expect(toolPolicy.isToolActive('Bash')).toBe(false); }); it('restores the denylist from persisted records on resume without catalog resolution', async () => { const persistence = new InMemoryWireRecordPersistence(); ctx = createTestAgent({ persistence }, hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile: 'deny-builtin', model: MOCK_MODEL }); await ctx.get(IWireService).flush(); await ctx.dispose(); const emptyCatalog = { _serviceBrand: undefined, ready: Promise.resolve(), get: () => undefined, getDefault: () => ({ name: DEFAULT_AGENT_PROFILE_NAME, tools: undefined, systemPrompt: () => '', }), list: () => [], load: async () => {}, reload: async () => {}, } as unknown as ISessionAgentProfileCatalog; ctx = createTestAgent( { persistence }, hostEnvironmentServices(homeDir), sessionService(ISessionAgentProfileCatalog, emptyCatalog), ); await ctx.restorePersisted(); const resumed = profileServices(ctx); expect(resumed.profile.data().profileName).toBe('deny-builtin'); expect(resumed.toolPolicy.isToolActive('Bash')).toBe(false); expect(resumed.toolPolicy.isToolActive('Read')).toBe(true); }); }); describe('AgentToolPolicyService global [tools] config', () => { beforeAll(() => { registerAgentProfile({ name: 'config-intersect', tools: ['Read', 'Bash'], disallowedTools: ['Bash'], systemPrompt: () => 'config intersect test', }); }); let ctx: TestAgentContext; let homeDir: string; beforeEach(async () => { homeDir = await mkdtemp(join(tmpdir(), 'kimi-tools-config-home-')); }); afterEach(async () => { await ctx?.dispose(); await rm(homeDir, { recursive: true, force: true }); }); async function bindWithToolsConfig( tools: Record, profile: string = DEFAULT_AGENT_PROFILE_NAME, ): Promise { ctx = createTestAgent({ initialConfig: { tools } }, hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile, model: MOCK_MODEL }); return ctx.get(IAgentToolPolicyService); } it('treats a non-empty enabled list as a global allowlist', async () => { const svc = await bindWithToolsConfig({ enabled: ['Read'] }); expect(svc.isToolActive('Read')).toBe(true); expect(svc.isToolActive('Bash')).toBe(false); }); it('treats an empty enabled list as unconstrained', async () => { const svc = await bindWithToolsConfig({ enabled: [] }); expect(svc.isToolActive('Read')).toBe(true); expect(svc.isToolActive('Bash')).toBe(true); }); it('applies disabled as a global denylist', async () => { const svc = await bindWithToolsConfig({ disabled: ['Bash'] }); expect(svc.isToolActive('Bash')).toBe(false); expect(svc.isToolActive('Read')).toBe(true); }); it('matches globally disabled mcp tools by glob', async () => { const svc = await bindWithToolsConfig({ disabled: ['mcp__github__*'] }); expect(svc.isToolActive('mcp__github__create_pr', 'mcp')).toBe(false); expect(svc.isToolActive('mcp__other__ping', 'mcp')).toBe(true); expect(svc.isToolActive('Read')).toBe(true); }); it('intersects the global config with the profile policy instead of overriding it', async () => { const svc = await bindWithToolsConfig({ enabled: ['Read', 'Bash'] }, 'config-intersect'); expect(svc.isToolActive('Read')).toBe(true); expect(svc.isToolActive('Bash')).toBe(false); expect(svc.isToolActive('Write')).toBe(false); }); }); describe('AgentToolPolicyService.setSessionDisabledTools', () => { beforeAll(() => { registerAgentProfile({ name: 'session-deny', disallowedTools: ['Write'], systemPrompt: () => 'session deny test', }); }); let ctx: TestAgentContext; let homeDir: string; beforeEach(async () => { homeDir = await mkdtemp(join(tmpdir(), 'kimi-session-deny-home-')); }); afterEach(async () => { await ctx?.dispose(); await rm(homeDir, { recursive: true, force: true }); }); async function bind(profile: string): Promise { ctx = createTestAgent(hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile, model: MOCK_MODEL }); return ctx.get(IAgentToolPolicyService); } it('rejects when no profile is bound yet', async () => { ctx = createTestAgent(hostEnvironmentServices(homeDir)); const toolPolicy = ctx.get(IAgentToolPolicyService); await expect(toolPolicy.setSessionDisabledTools(['Bash'])).rejects.toThrow(/not bound/); expect(toolPolicy.isToolActive('Bash')).toBe(true); }); it('replaces the client-managed denylist on every call', async () => { const svc = await bind(DEFAULT_AGENT_PROFILE_NAME); await svc.setSessionDisabledTools(['Bash']); expect(svc.isToolActive('Bash')).toBe(false); expect(svc.isToolActive('Read')).toBe(true); await svc.setSessionDisabledTools(['Edit']); expect(svc.isToolActive('Bash')).toBe(true); expect(svc.isToolActive('Edit')).toBe(false); }); it('keeps the profile own denylist across replacement calls', async () => { const svc = await bind('session-deny'); await svc.setSessionDisabledTools(['Bash']); expect(svc.isToolActive('Write')).toBe(false); expect(svc.isToolActive('Bash')).toBe(false); await svc.setSessionDisabledTools([]); expect(svc.isToolActive('Write')).toBe(false); expect(svc.isToolActive('Bash')).toBe(true); }); it('persists the session denylist across a resume', async () => { const persistence = new InMemoryWireRecordPersistence(); const atomicDocuments = createAtomicDocumentStore(); const documentServices = appService(IAtomicDocumentStore, atomicDocuments); ctx = createTestAgent( { persistence }, documentServices, hostEnvironmentServices(homeDir), ); const { profile, toolPolicy } = profileServices(ctx); await profile.bind({ profile: 'session-deny', model: MOCK_MODEL }); await toolPolicy.setSessionDisabledTools(['Bash']); await ctx.get(IWireService).flush(); await ctx.dispose(); const emptyCatalog = { _serviceBrand: undefined, ready: Promise.resolve(), get: () => undefined, getDefault: () => ({ name: DEFAULT_AGENT_PROFILE_NAME, tools: undefined, systemPrompt: () => '', }), list: () => [], load: async () => {}, reload: async () => {}, } as unknown as ISessionAgentProfileCatalog; ctx = createTestAgent( { persistence }, documentServices, hostEnvironmentServices(homeDir), sessionService(ISessionAgentProfileCatalog, emptyCatalog), ); await ctx.restorePersisted(); await ctx.get(ISessionToolPolicy).ready; const resumed = profileServices(ctx); expect(resumed.toolPolicy.isToolActive('Bash')).toBe(false); expect(resumed.toolPolicy.isToolActive('Write')).toBe(false); expect(resumed.toolPolicy.isToolActive('Read')).toBe(true); await resumed.toolPolicy.setSessionDisabledTools(['Edit']); expect(resumed.toolPolicy.isToolActive('Bash')).toBe(true); expect(resumed.toolPolicy.isToolActive('Edit')).toBe(false); expect(resumed.toolPolicy.isToolActive('Write')).toBe(false); }); it('retries persistence after a failed session denylist replacement', async () => { const atomicDocuments = createAtomicDocumentStore(); const persist = atomicDocuments.set.bind(atomicDocuments); let attempts = 0; atomicDocuments.set = async (...args) => { if (args[0].endsWith('/tool-policy')) { attempts += 1; if (attempts === 1) throw new Error('disk full'); } await persist(...args); }; ctx = createTestAgent( appService(IAtomicDocumentStore, atomicDocuments), hostEnvironmentServices(homeDir), ); const { profile, toolPolicy } = profileServices(ctx); await profile.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); await expect(toolPolicy.setSessionDisabledTools(['Bash'])).rejects.toThrow('disk full'); expect(toolPolicy.isToolActive('Bash')).toBe(true); await toolPolicy.setSessionDisabledTools(['Bash']); expect(attempts).toBe(2); expect(toolPolicy.isToolActive('Bash')).toBe(false); }); it('keeps the skill listing frozen when the session disables Skill', async () => { const skillMarker = 'session-policy-skill-marker'; ctx = createTestAgent( hostEnvironmentServices(homeDir), sessionService(ISessionSkillCatalog, { _serviceBrand: undefined, catalog: { getModelSkillListing: () => skillMarker } as never, ready: Promise.resolve(), onDidChange: Event.None as Event, load: async () => {}, reload: async () => {}, list: async () => [], }), ); const { profile, toolPolicy } = profileServices(ctx); await profile.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); const before = profile.getSystemPrompt(); expect(before).toContain(skillMarker); await toolPolicy.setSessionDisabledTools(['Skill']); expect(toolPolicy.isToolActive('Skill')).toBe(false); expect(profile.getSystemPrompt()).toBe(before); }); it('omits the skill listing when global tools disable Skill', async () => { const skillMarker = 'global-policy-skill-marker'; ctx = createTestAgent( { initialConfig: { tools: { disabled: ['Skill'] } } }, hostEnvironmentServices(homeDir), sessionService(ISessionSkillCatalog, { _serviceBrand: undefined, catalog: { getModelSkillListing: () => skillMarker } as never, ready: Promise.resolve(), onDidChange: Event.None as Event, load: async () => {}, reload: async () => {}, list: async () => [], }), ); const { profile, toolPolicy } = profileServices(ctx); await profile.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); expect(toolPolicy.isToolActive('Skill')).toBe(false); expect(profile.getSystemPrompt()).not.toContain(skillMarker); }); it('keeps the skill listing frozen when global tool policy changes at runtime', async () => { const skillMarker = 'live-global-policy-skill-marker'; ctx = createTestAgent( hostEnvironmentServices(homeDir), sessionService(ISessionSkillCatalog, { _serviceBrand: undefined, catalog: { getModelSkillListing: () => skillMarker } as never, ready: Promise.resolve(), onDidChange: Event.None as Event, load: async () => {}, reload: async () => {}, list: async () => [], }), ); const { profile, toolPolicy } = profileServices(ctx); await profile.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); const before = profile.getSystemPrompt(); expect(before).toContain(skillMarker); await ctx .get(IConfigService) .replace(TOOLS_SECTION, { disabled: ['Skill'] }, ConfigTarget.Memory); expect(toolPolicy.isToolActive('Skill')).toBe(false); expect(profile.getSystemPrompt()).toBe(before); }); }); describe('AgentToolPolicyService executor enforcement', () => { let ctx: TestAgentContext; let homeDir: string; beforeAll(() => { registerAgentProfile({ name: 'executor-deny-builtin', disallowedTools: ['PolicyProbe'], systemPrompt: () => 'executor policy test', }); registerAgentProfile({ name: 'executor-deny-mcp', disallowedTools: ['mcp__blocked__*'], systemPrompt: () => 'executor policy test', }); }); beforeEach(async () => { homeDir = await mkdtemp(join(tmpdir(), 'kimi-executor-policy-home-')); }); afterEach(async () => { await ctx?.dispose(); await rm(homeDir, { recursive: true, force: true }); }); it.each([ { name: 'profile denylist', options: {}, profile: 'executor-deny-builtin', disable: undefined, }, { name: 'global tools config', options: { initialConfig: { tools: { disabled: ['PolicyProbe'] } } }, profile: DEFAULT_AGENT_PROFILE_NAME, disable: undefined, }, { name: 'session denylist', options: {}, profile: DEFAULT_AGENT_PROFILE_NAME, disable: ['PolicyProbe'], }, ])('blocks a direct builtin call through $name', async ({ options, profile, disable }) => { ctx = createTestAgent(options, hostEnvironmentServices(homeDir)); const profileService = ctx.get(IAgentProfileService); await profileService.bind({ profile, model: MOCK_MODEL }); if (disable !== undefined) { await ctx.get(IAgentToolPolicyService).setSessionDisabledTools(disable); } const probe = new PolicyProbeTool('PolicyProbe'); ctx.get(IAgentToolRegistryService).register(probe); const result = await executeDirectToolCall(ctx, 'PolicyProbe'); expect(result).toMatchObject({ isError: true, output: 'Tool "PolicyProbe" is disabled by the active tool policy', }); expect(probe.calls).toBe(0); }); it('blocks a direct MCP call by glob before execution', async () => { ctx = createTestAgent(hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile: 'executor-deny-mcp', model: MOCK_MODEL }); const probe = new PolicyProbeTool('mcp__blocked__write'); ctx.get(IAgentToolRegistryService).register(probe, { source: 'mcp' }); const result = await executeDirectToolCall(ctx, probe.name); expect(result).toMatchObject({ isError: true, output: `Tool "${probe.name}" is disabled by the active tool policy`, }); expect(probe.calls).toBe(0); }); it('blocks a direct builtin call through the workspace tool-policy gate', async () => { ctx = createTestAgent( hostEnvironmentServices(homeDir), sessionService(ISessionToolPolicyGate, { _serviceBrand: undefined, disabledTools: ['PolicyProbe'], onDidChange: Event.None as Event, } satisfies ISessionToolPolicyGate), ); await ctx.get(IAgentProfileService).bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL, }); const probe = new PolicyProbeTool('PolicyProbe'); ctx.get(IAgentToolRegistryService).register(probe); const result = await executeDirectToolCall(ctx, 'PolicyProbe'); expect(result).toMatchObject({ isError: true, output: 'Tool "PolicyProbe" is disabled by the active tool policy', }); expect(probe.calls).toBe(0); }); it('applies the workspace gate in the prompt projection (skillActive)', async () => { registerAgentProfile({ name: 'gate-skill-active', tools: ['Read', 'Skill'], systemPrompt: (context) => `skill-active:${String(context.skillActive)}`, }); ctx = createTestAgent( hostEnvironmentServices(homeDir), sessionService(ISessionToolPolicyGate, { _serviceBrand: undefined, disabledTools: ['Skill'], onDidChange: Event.None as Event, } satisfies ISessionToolPolicyGate), ); const profileService = ctx.get(IAgentProfileService); await profileService.bind({ profile: 'gate-skill-active', model: MOCK_MODEL }); expect(profileService.data().systemPrompt).toBe('skill-active:false'); }); it('does not reject select_tools, the policy-gated disclosure loading entry', async () => { ctx = createTestAgent(hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); const probe = new PolicyProbeTool(SELECT_TOOLS_TOOL_NAME); ctx.get(IAgentToolRegistryService).register(probe); const result = await executeDirectToolCall(ctx, SELECT_TOOLS_TOOL_NAME); expect(result).toMatchObject({ output: 'executed' }); expect(result.isError).toBeFalsy(); expect(probe.calls).toBe(1); }); it.each([ { name: 'global denylist', options: { initialConfig: { tools: { disabled: [SELECT_TOOLS_TOOL_NAME] } } }, disable: undefined, }, { name: 'global allowlist', options: { initialConfig: { tools: { enabled: ['Read'] } } }, disable: undefined, }, { name: 'session denylist', options: {}, disable: [SELECT_TOOLS_TOOL_NAME], }, ])('blocks select_tools through an explicit $name', async ({ options, disable }) => { ctx = createTestAgent(options, hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL, }); if (disable !== undefined) { await ctx.get(IAgentToolPolicyService).setSessionDisabledTools(disable); } const probe = new PolicyProbeTool(SELECT_TOOLS_TOOL_NAME); ctx.get(IAgentToolRegistryService).register(probe); const result = await executeDirectToolCall(ctx, SELECT_TOOLS_TOOL_NAME); expect(result).toMatchObject({ isError: true, output: `Tool "${SELECT_TOOLS_TOOL_NAME}" is disabled by the active tool policy`, }); expect(probe.calls).toBe(0); }); }); describe('AgentProfileService tool-pattern warnings', () => { let ctx: TestAgentContext; let homeDir: string; beforeEach(async () => { homeDir = await mkdtemp(join(tmpdir(), 'kimi-tool-pattern-home-')); }); afterEach(async () => { await ctx?.dispose(); await rm(homeDir, { recursive: true, force: true }); }); function toolPatternWarnings(): readonly { code?: string; message?: string }[] { const events = ctx.newEvents() as readonly { event: string; args?: { code?: string; message?: string }; }[]; return events .filter((entry) => entry.event === 'warning') .map((entry) => entry.args ?? {}) .filter((args) => args.code === 'tool-pattern-no-match'); } const fileProfile: ResolvedAgentProfile = normalizeAgentProfile({ name: 'bad-patterns', tools: ['Bashh', 'mcp__github'], disallowedTools: ['*'], systemPrompt: () => 'tool pattern warning test', }); it('warns about profile entries that can never activate anything', async () => { ctx = createTestAgent(hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).applyProfile(fileProfile); const messages = toolPatternWarnings().map((warning) => warning.message ?? ''); expect( messages.some((m) => m.includes('"Bashh"') && m.includes('profile "bad-patterns"')), ).toBe(true); expect(messages.some((m) => m.includes('"mcp__github"') && m.includes('mcp__github__*'))).toBe( true, ); expect(messages.some((m) => m.includes('"*"') && m.includes('disallowedTools'))).toBe(true); }); it('warns once per pattern across repeated applications of the same profile', async () => { ctx = createTestAgent(hostEnvironmentServices(homeDir)); const svc = ctx.get(IAgentProfileService); await svc.applyProfile(fileProfile); await svc.applyProfile(fileProfile); const messages = toolPatternWarnings().map((warning) => warning.message ?? ''); expect(messages.filter((m) => m.includes('"Bashh"'))).toHaveLength(1); }); it('warns about global [tools] config entries that can never activate anything', async () => { ctx = createTestAgent( { initialConfig: { tools: { enabled: ['*'] } } }, hostEnvironmentServices(homeDir), ); await ctx.get(IAgentProfileService).bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); const messages = toolPatternWarnings().map((warning) => warning.message ?? ''); expect( messages.some( (m) => m.includes('"*"') && m.includes('the global [tools] config') && m.includes('enabled'), ), ).toBe(true); }); it('stays silent for the default profile and an empty [tools] config', async () => { ctx = createTestAgent(hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); expect(toolPatternWarnings()).toEqual([]); }); it('bind also publishes the warnings', async () => { registerAgentProfile({ name: 'bind-bad-patterns', tools: ['mcp__github'], disallowedTools: ['*'], systemPrompt: () => 'bind warning test', }); ctx = createTestAgent(hostEnvironmentServices(homeDir)); await ctx.get(IAgentProfileService).bind({ profile: 'bind-bad-patterns', model: MOCK_MODEL }); const messages = toolPatternWarnings().map((warning) => warning.message ?? ''); expect(messages.some((m) => m.includes('"mcp__github"') && m.includes('mcp__github__*'))).toBe( true, ); expect(messages.some((m) => m.includes('"*"') && m.includes('disallowedTools'))).toBe(true); }); }); async function executeDirectToolCall(ctx: TestAgentContext, name: string): Promise { const call: ToolCall = { type: 'function', id: `call_${name}`, name, arguments: '{}', }; for await (const result of ctx.get(IAgentToolExecutorService).execute([call], { signal: new AbortController().signal, turnId: 1, })) { return result.result; } throw new Error(`No result for tool ${name}`); } class PolicyProbeTool implements ExecutableTool> { readonly description = 'Policy enforcement probe.'; readonly parameters = { type: 'object', additionalProperties: false }; calls = 0; constructor( readonly name: string, readonly source?: ToolSource, ) {} resolveExecution(): ToolExecution { return { approvalRule: this.name, execute: async () => { this.calls += 1; return { isError: false, output: 'executed' }; }, }; } } describe('agentsMdReminder seeding', () => { let ctx: TestAgentContext; let homeDir: string; let workDir: string; beforeAll(() => { registerAgentProfile({ name: 'throws-on-prompt', systemPrompt: () => { throw new Error('prompt build boom'); }, }); }); beforeEach(async () => { homeDir = await mkdtemp(join(tmpdir(), 'kimi-seed-home-')); workDir = await mkdtemp(join(tmpdir(), 'kimi-seed-work-')); }); afterEach(async () => { await ctx?.dispose(); await rm(homeDir, { recursive: true, force: true }); await rm(workDir, { recursive: true, force: true }); }); function buildSeededContext( seedInjected: IAgentAgentsMdReminderService['seedInjected'], ): IAgentProfileService { ctx = createTestAgent( { cwd: workDir }, hostEnvironmentServices(homeDir), agentService(IAgentAgentsMdReminderService, { _serviceBrand: undefined, seedInjected, }), ); return ctx.get(IAgentProfileService); } it('seeds the known-set with the injected paths after a successful bind', async () => { const seedInjected = vi.fn<(paths: readonly string[], cwd: string) => void>(); const profile = buildSeededContext(seedInjected); await writeFile(join(workDir, 'AGENTS.md'), 'project instructions', 'utf-8'); seedInjected.mockClear(); await profile.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: MOCK_MODEL }); expect(seedInjected).toHaveBeenCalledWith([normalize(join(workDir, 'AGENTS.md'))], workDir); expect(profile.data().agentsMdPaths).toEqual([normalize(join(workDir, 'AGENTS.md'))]); }); it('does not seed when the prompt build fails before the bind commits', async () => { const seedInjected = vi.fn<(paths: readonly string[], cwd: string) => void>(); const profile = buildSeededContext(seedInjected); seedInjected.mockClear(); await expect(profile.bind({ profile: 'throws-on-prompt', model: MOCK_MODEL })).rejects.toThrow( 'prompt build boom', ); expect(seedInjected).not.toHaveBeenCalled(); }); });