Compare commits

...

2 Commits

Author SHA1 Message Date
Taylor Mullen 7ef1a3f5ea fix(core): update tests to match tool changes and resolve timeouts 2026-02-09 01:09:23 -08:00
Taylor Mullen 93f9e89114 fix(core): enforce coreTools restrictions for sub-agents and skills
- ensure sub-agent tools and ActivateSkillTool respect the coreTools configuration
- prevents unauthorized tool calls when using restricted toolsets, particularly with Gemini 3
- added missing web_fetch tool to CodebaseInvestigatorAgent toolConfig to match its system prompt

These changes resolve several behavioral evaluation failures (e.g., save_memory) that occurred when the agent attempted to use incorrectly registered sub-agents or skills in restricted environments.
2026-02-09 01:09:23 -08:00
4 changed files with 35 additions and 12 deletions
@@ -11,6 +11,7 @@ import {
GREP_TOOL_NAME,
LS_TOOL_NAME,
READ_FILE_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
} from '../tools/tool-names.js';
import { DEFAULT_GEMINI_MODEL } from '../config/models.js';
import { makeFakeConfig } from '../test-utils/config.js';
@@ -50,6 +51,7 @@ describe('CodebaseInvestigatorAgent', () => {
READ_FILE_TOOL_NAME,
GLOB_TOOL_NAME,
GREP_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
]);
});
@@ -10,6 +10,7 @@ import {
GREP_TOOL_NAME,
LS_TOOL_NAME,
READ_FILE_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
} from '../tools/tool-names.js';
import {
DEFAULT_THINKING_MODE,
@@ -120,6 +121,7 @@ export const CodebaseInvestigatorAgent = (
READ_FILE_TOOL_NAME,
GLOB_TOOL_NAME,
GREP_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
],
},
+26 -3
View File
@@ -947,7 +947,10 @@ export class Config {
this.getSkillManager().setDisabledSkills(this.disabledSkills);
// Re-register ActivateSkillTool to update its schema with the discovered enabled skill enums
if (this.getSkillManager().getSkills().length > 0) {
if (
this.getSkillManager().getSkills().length > 0 &&
this.isToolEnabledByCore(ActivateSkillTool.Name)
) {
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
this.getToolRegistry().registerTool(
new ActivateSkillTool(this, this.messageBus),
@@ -971,6 +974,21 @@ export class Config {
this.syncPlanModeTools();
}
/**
* Returns true if the specified tool name is enabled based on the current
* coreTools configuration.
*/
private isToolEnabledByCore(toolName: string): boolean {
const coreTools = this.getCoreTools();
if (!coreTools) {
return true;
}
return coreTools.some(
(name) => name === toolName || name === toolName.toLowerCase(),
);
}
getContentGenerator(): ContentGenerator {
return this.contentGenerator;
}
@@ -2002,7 +2020,10 @@ export class Config {
this.getSkillManager().setDisabledSkills(this.disabledSkills);
// Re-register ActivateSkillTool to update its schema with the newly discovered skills
if (this.getSkillManager().getSkills().length > 0) {
if (
this.getSkillManager().getSkills().length > 0 &&
this.isToolEnabledByCore(ActivateSkillTool.Name)
) {
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
this.getToolRegistry().registerTool(
new ActivateSkillTool(this, this.messageBus),
@@ -2234,8 +2255,10 @@ export class Config {
const definitions = this.agentRegistry.getAllDefinitions();
for (const definition of definitions) {
const isAllowed =
const isAllowedByTools =
!allowedTools || allowedTools.includes(definition.name);
const isAllowed =
isAllowedByTools && this.isToolEnabledByCore(definition.name);
if (isAllowed) {
try {
+5 -9
View File
@@ -12,6 +12,10 @@ import type { PolicySettings } from './types.js';
import { ApprovalMode, PolicyDecision, InProcessCheckerType } from './types.js';
import { isDirectorySecure } from '../utils/security.js';
import { Storage } from '../config/storage.js';
import * as tomlLoader from './toml-loader.js';
import { createPolicyEngineConfig } from './config.js';
vi.unmock('../config/storage.js');
vi.mock('../utils/security.js', () => ({
@@ -26,8 +30,6 @@ afterEach(() => {
describe('createPolicyEngineConfig', () => {
beforeEach(async () => {
vi.resetModules();
const { Storage } = await import('../config/storage.js');
// Mock Storage to avoid picking up real user/system policies from the host environment
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(
'/non/existent/user/policies',
@@ -40,7 +42,6 @@ describe('createPolicyEngineConfig', () => {
});
it('should filter out insecure system policy directories', async () => {
const { Storage } = await import('../config/storage.js');
const systemPolicyDir = '/insecure/system/policies';
vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(systemPolicyDir);
@@ -51,10 +52,6 @@ describe('createPolicyEngineConfig', () => {
return { secure: true };
});
// We need to spy on loadPoliciesFromToml to verify which directories were passed
// But it is not exported from config.js, it is imported.
// We can spy on the module it comes from.
const tomlLoader = await import('./toml-loader.js');
const loadPoliciesSpy = vi.spyOn(tomlLoader, 'loadPoliciesFromToml');
loadPoliciesSpy.mockResolvedValue({
rules: [],
@@ -62,7 +59,6 @@ describe('createPolicyEngineConfig', () => {
errors: [],
});
const { createPolicyEngineConfig } = await import('./config.js');
const settings: PolicySettings = {};
await createPolicyEngineConfig(
@@ -80,7 +76,7 @@ describe('createPolicyEngineConfig', () => {
// But other directories (user, default) should be there
expect(calledDirs).toContain('/non/existent/user/policies');
expect(calledDirs).toContain('/tmp/mock/default/policies');
});
}, 30000);
it('should return ASK_USER for write tools and ALLOW for read-only tools by default', async () => {
const actualFs =