feat(gmail): add filter creation tool and auth scope
This commit is contained in:
@@ -72,6 +72,7 @@ describe('gmail-auth', () => {
|
|||||||
expect(url).toContain('client_id=my-client-id');
|
expect(url).toContain('client_id=my-client-id');
|
||||||
expect(url).toContain('redirect_uri=http%3A%2F%2Flocalhost%3A3000');
|
expect(url).toContain('redirect_uri=http%3A%2F%2Flocalhost%3A3000');
|
||||||
expect(url).toContain('scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.readonly');
|
expect(url).toContain('scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.readonly');
|
||||||
|
expect(url).toContain('https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.settings.basic');
|
||||||
expect(url).toContain('access_type=offline');
|
expect(url).toContain('access_type=offline');
|
||||||
expect(url).toContain('prompt=consent');
|
expect(url).toContain('prompt=consent');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { createServer, type Server } from 'http';
|
|||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { loadConfigSafe } from './shared.js';
|
import { loadConfigSafe } from './shared.js';
|
||||||
|
|
||||||
const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly'];
|
const SCOPES = [
|
||||||
|
'https://www.googleapis.com/auth/gmail.readonly',
|
||||||
|
'https://www.googleapis.com/auth/gmail.settings.basic',
|
||||||
|
];
|
||||||
const REDIRECT_PORT = 3000;
|
const REDIRECT_PORT = 3000;
|
||||||
const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}`;
|
const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}`;
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|||||||
import type { GmailConfig } from '../../config/schema.js';
|
import type { GmailConfig } from '../../config/schema.js';
|
||||||
|
|
||||||
// Hoisted mocks so vi.mock factories can reference them
|
// Hoisted mocks so vi.mock factories can reference them
|
||||||
const { mockMessagesList, mockMessagesGet, mockExistsSync, mockReadFileSync } = vi.hoisted(() => ({
|
const { mockMessagesList, mockMessagesGet, mockFiltersCreate, mockExistsSync, mockReadFileSync } = vi.hoisted(() => ({
|
||||||
mockMessagesList: vi.fn(),
|
mockMessagesList: vi.fn(),
|
||||||
mockMessagesGet: vi.fn(),
|
mockMessagesGet: vi.fn(),
|
||||||
|
mockFiltersCreate: vi.fn(),
|
||||||
mockExistsSync: vi.fn(),
|
mockExistsSync: vi.fn(),
|
||||||
mockReadFileSync: vi.fn(),
|
mockReadFileSync: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -22,6 +23,11 @@ vi.mock('googleapis', () => ({
|
|||||||
list: mockMessagesList,
|
list: mockMessagesList,
|
||||||
get: mockMessagesGet,
|
get: mockMessagesGet,
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
filters: {
|
||||||
|
create: mockFiltersCreate,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -100,10 +106,10 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createGmailTools', () => {
|
describe('createGmailTools', () => {
|
||||||
it('returns 3 tools with correct names', () => {
|
it('returns 4 tools with correct names', () => {
|
||||||
const tools = createGmailTools(testConfig);
|
const tools = createGmailTools(testConfig);
|
||||||
expect(tools).toHaveLength(3);
|
expect(tools).toHaveLength(4);
|
||||||
expect(tools.map(t => t.name)).toEqual(['gmail.list', 'gmail.search', 'gmail.read']);
|
expect(tools.map(t => t.name)).toEqual(['gmail.list', 'gmail.search', 'gmail.read', 'gmail.filter.create']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tools have descriptions and input schemas', () => {
|
it('tools have descriptions and input schemas', () => {
|
||||||
@@ -116,6 +122,106 @@ describe('createGmailTools', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('gmail.filter.create', () => {
|
||||||
|
it('creates a Gmail filter with criteria + actions', async () => {
|
||||||
|
setupValidAuth();
|
||||||
|
mockFiltersCreate.mockResolvedValue({
|
||||||
|
data: { id: 'filter-123' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const [, , , filterCreateTool] = createGmailTools(testConfig);
|
||||||
|
const result = await filterCreateTool.execute({
|
||||||
|
from: 'billing@example.com',
|
||||||
|
query: 'subject:invoice',
|
||||||
|
addLabelIds: ['IMPORTANT'],
|
||||||
|
removeLabelIds: ['INBOX'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.output).toContain('Gmail filter created: filter-123');
|
||||||
|
expect(result.output).toContain('from=billing@example.com');
|
||||||
|
expect(result.output).toContain('query=subject:invoice');
|
||||||
|
expect(result.output).toContain('addLabelIds=[IMPORTANT]');
|
||||||
|
expect(result.output).toContain('removeLabelIds=[INBOX]');
|
||||||
|
|
||||||
|
expect(mockFiltersCreate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
userId: 'me',
|
||||||
|
requestBody: expect.objectContaining({
|
||||||
|
criteria: expect.objectContaining({
|
||||||
|
from: 'billing@example.com',
|
||||||
|
query: 'subject:invoice',
|
||||||
|
}),
|
||||||
|
action: expect.objectContaining({
|
||||||
|
addLabelIds: ['IMPORTANT'],
|
||||||
|
removeLabelIds: ['INBOX'],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when no criteria provided', async () => {
|
||||||
|
setupValidAuth();
|
||||||
|
|
||||||
|
const [, , , filterCreateTool] = createGmailTools(testConfig);
|
||||||
|
const result = await filterCreateTool.execute({
|
||||||
|
addLabelIds: ['IMPORTANT'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('At least one filter criterion is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when no action provided', async () => {
|
||||||
|
setupValidAuth();
|
||||||
|
|
||||||
|
const [, , , filterCreateTool] = createGmailTools(testConfig);
|
||||||
|
const result = await filterCreateTool.execute({
|
||||||
|
from: 'news@example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('At least one filter action is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates size + sizeComparison dependency', async () => {
|
||||||
|
setupValidAuth();
|
||||||
|
const [, , , filterCreateTool] = createGmailTools(testConfig);
|
||||||
|
|
||||||
|
const sizeWithoutComparison = await filterCreateTool.execute({
|
||||||
|
from: 'alerts@example.com',
|
||||||
|
size: 500000,
|
||||||
|
removeLabelIds: ['INBOX'],
|
||||||
|
});
|
||||||
|
expect(sizeWithoutComparison.success).toBe(false);
|
||||||
|
expect(sizeWithoutComparison.error).toContain('sizeComparison is required');
|
||||||
|
|
||||||
|
const comparisonWithoutSize = await filterCreateTool.execute({
|
||||||
|
from: 'alerts@example.com',
|
||||||
|
sizeComparison: 'larger',
|
||||||
|
removeLabelIds: ['INBOX'],
|
||||||
|
});
|
||||||
|
expect(comparisonWithoutSize.success).toBe(false);
|
||||||
|
expect(comparisonWithoutSize.error).toContain('size is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('surfaces re-auth hint for insufficient scopes', async () => {
|
||||||
|
setupValidAuth();
|
||||||
|
mockFiltersCreate.mockRejectedValue(new Error('Request had insufficient authentication scopes.'));
|
||||||
|
|
||||||
|
const [, , , filterCreateTool] = createGmailTools(testConfig);
|
||||||
|
const result = await filterCreateTool.execute({
|
||||||
|
from: 'alerts@example.com',
|
||||||
|
removeLabelIds: ['INBOX'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('insufficient authentication scopes');
|
||||||
|
expect(result.error).toContain('flynn gmail-auth');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('gmail.list', () => {
|
describe('gmail.list', () => {
|
||||||
it('returns error when credentials file missing', async () => {
|
it('returns error when credentials file missing', async () => {
|
||||||
mockExistsSync.mockReturnValue(false);
|
mockExistsSync.mockReturnValue(false);
|
||||||
|
|||||||
+233
-1
@@ -58,6 +58,21 @@ interface EmailSummary {
|
|||||||
snippet: string;
|
snippet: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GmailFilterCreateArgs {
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
subject?: string;
|
||||||
|
query?: string;
|
||||||
|
negatedQuery?: string;
|
||||||
|
hasAttachment?: boolean;
|
||||||
|
excludeChats?: boolean;
|
||||||
|
size?: number;
|
||||||
|
sizeComparison?: 'larger' | 'smaller';
|
||||||
|
addLabelIds?: string[];
|
||||||
|
removeLabelIds?: string[];
|
||||||
|
forward?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** Fetch metadata for a single message. */
|
/** Fetch metadata for a single message. */
|
||||||
async function fetchMessageDetails(
|
async function fetchMessageDetails(
|
||||||
gmail: ReturnType<typeof google.gmail>,
|
gmail: ReturnType<typeof google.gmail>,
|
||||||
@@ -143,6 +158,58 @@ function formatEmails(emails: EmailSummary[]): string {
|
|||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCreatedFilter(args: GmailFilterCreateArgs, filterId: string): string {
|
||||||
|
const criteriaParts: string[] = [];
|
||||||
|
if (args.from) {criteriaParts.push(`from=${args.from}`);}
|
||||||
|
if (args.to) {criteriaParts.push(`to=${args.to}`);}
|
||||||
|
if (args.subject) {criteriaParts.push(`subject=${args.subject}`);}
|
||||||
|
if (args.query) {criteriaParts.push(`query=${args.query}`);}
|
||||||
|
if (args.negatedQuery) {criteriaParts.push(`negatedQuery=${args.negatedQuery}`);}
|
||||||
|
if (typeof args.hasAttachment === 'boolean') {criteriaParts.push(`hasAttachment=${args.hasAttachment}`);}
|
||||||
|
if (typeof args.excludeChats === 'boolean') {criteriaParts.push(`excludeChats=${args.excludeChats}`);}
|
||||||
|
if (typeof args.size === 'number') {
|
||||||
|
criteriaParts.push(`size=${args.size}`);
|
||||||
|
}
|
||||||
|
if (args.sizeComparison) {
|
||||||
|
criteriaParts.push(`sizeComparison=${args.sizeComparison}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionParts: string[] = [];
|
||||||
|
if (args.addLabelIds && args.addLabelIds.length > 0) {
|
||||||
|
actionParts.push(`addLabelIds=[${args.addLabelIds.join(', ')}]`);
|
||||||
|
}
|
||||||
|
if (args.removeLabelIds && args.removeLabelIds.length > 0) {
|
||||||
|
actionParts.push(`removeLabelIds=[${args.removeLabelIds.join(', ')}]`);
|
||||||
|
}
|
||||||
|
if (args.forward) {actionParts.push(`forward=${args.forward}`);}
|
||||||
|
|
||||||
|
const output = [
|
||||||
|
`Gmail filter created: ${filterId}`,
|
||||||
|
`Criteria: ${criteriaParts.join(', ')}`,
|
||||||
|
`Action: ${actionParts.join(', ')}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
return output.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStringArray(values: unknown): string[] {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
.filter((v): v is string => typeof v === 'string')
|
||||||
|
.map(v => v.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates Gmail query tools bound to the given GmailConfig.
|
* Creates Gmail query tools bound to the given GmailConfig.
|
||||||
* Tools create their own OAuth2 client, independent of GmailWatcher.
|
* Tools create their own OAuth2 client, independent of GmailWatcher.
|
||||||
@@ -317,5 +384,170 @@ export function createGmailTools(config: NonNullable<GmailConfig>): Tool[] {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return [gmailList, gmailSearch, gmailRead];
|
const gmailFilterCreate: Tool = {
|
||||||
|
name: 'gmail.filter.create',
|
||||||
|
description:
|
||||||
|
'Create a Gmail filter rule using Gmail API criteria and actions (e.g. archive, apply labels, forward). Requires Gmail auth with filter-management scope.',
|
||||||
|
requiredSecretScopes: ['gmail'],
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
from: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter by sender email/address text',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter by recipient email/address text',
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter by subject text',
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Gmail search query (supports operators like is:unread, has:attachment)',
|
||||||
|
},
|
||||||
|
negatedQuery: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Gmail search query that must NOT match',
|
||||||
|
},
|
||||||
|
hasAttachment: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Match messages that have attachments',
|
||||||
|
},
|
||||||
|
excludeChats: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Exclude Google Chat messages',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Message size threshold in bytes (requires sizeComparison)',
|
||||||
|
},
|
||||||
|
sizeComparison: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['larger', 'smaller'],
|
||||||
|
description: 'How to compare size threshold',
|
||||||
|
},
|
||||||
|
addLabelIds: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Label IDs to add when matched (system labels like INBOX/IMPORTANT also valid)',
|
||||||
|
items: { type: 'string' },
|
||||||
|
},
|
||||||
|
removeLabelIds: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Label IDs to remove when matched (e.g. INBOX to archive)',
|
||||||
|
items: { type: 'string' },
|
||||||
|
},
|
||||||
|
forward: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Forward matched mail to this pre-verified forwarding address',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||||
|
const args = rawArgs as GmailFilterCreateArgs;
|
||||||
|
|
||||||
|
const hasCriteria = Boolean(
|
||||||
|
args.from?.trim()
|
||||||
|
|| args.to?.trim()
|
||||||
|
|| args.subject?.trim()
|
||||||
|
|| args.query?.trim()
|
||||||
|
|| args.negatedQuery?.trim()
|
||||||
|
|| typeof args.hasAttachment === 'boolean'
|
||||||
|
|| typeof args.excludeChats === 'boolean'
|
||||||
|
|| typeof args.size === 'number'
|
||||||
|
|| args.sizeComparison,
|
||||||
|
);
|
||||||
|
if (!hasCriteria) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: 'At least one filter criterion is required (e.g. from, subject, query, hasAttachment).',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args.size === 'number' && !args.sizeComparison) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: 'sizeComparison is required when size is provided ("larger" or "smaller").',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.sizeComparison && typeof args.size !== 'number') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: 'size is required when sizeComparison is provided.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const addLabelIds = normalizeStringArray(args.addLabelIds);
|
||||||
|
const removeLabelIds = normalizeStringArray(args.removeLabelIds);
|
||||||
|
const forward = args.forward?.trim();
|
||||||
|
|
||||||
|
const hasAction = addLabelIds.length > 0 || removeLabelIds.length > 0 || Boolean(forward);
|
||||||
|
if (!hasAction) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: 'At least one filter action is required (addLabelIds, removeLabelIds, or forward).',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auth = createOAuth2Client(config);
|
||||||
|
const gmail = google.gmail({ version: 'v1', auth });
|
||||||
|
|
||||||
|
const createResponse = await gmail.users.settings.filters.create({
|
||||||
|
userId: 'me',
|
||||||
|
requestBody: {
|
||||||
|
criteria: {
|
||||||
|
from: args.from?.trim(),
|
||||||
|
to: args.to?.trim(),
|
||||||
|
subject: args.subject?.trim(),
|
||||||
|
query: args.query?.trim(),
|
||||||
|
negatedQuery: args.negatedQuery?.trim(),
|
||||||
|
hasAttachment: args.hasAttachment,
|
||||||
|
excludeChats: args.excludeChats,
|
||||||
|
size: args.size,
|
||||||
|
sizeComparison: args.sizeComparison,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
addLabelIds: addLabelIds.length > 0 ? addLabelIds : undefined,
|
||||||
|
removeLabelIds: removeLabelIds.length > 0 ? removeLabelIds : undefined,
|
||||||
|
forward,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterId = createResponse.data.id ?? 'unknown-id';
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: formatCreatedFilter(
|
||||||
|
{
|
||||||
|
...args,
|
||||||
|
addLabelIds,
|
||||||
|
removeLabelIds,
|
||||||
|
forward,
|
||||||
|
},
|
||||||
|
filterId,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = parseErrorMessage(error);
|
||||||
|
const needsScopeHint = /insufficient.*scope|insufficientPermissions|Request had insufficient authentication scopes/i.test(message);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: needsScopeHint
|
||||||
|
? `${message}. Re-run "flynn gmail-auth" to grant Gmail filter-management permissions.`
|
||||||
|
: message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [gmailList, gmailSearch, gmailRead, gmailFilterCreate];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const ALL_TOOL_NAMES = [
|
|||||||
'memory.read',
|
'memory.read',
|
||||||
'memory.write',
|
'memory.write',
|
||||||
'memory.search',
|
'memory.search',
|
||||||
|
'gmail.list',
|
||||||
|
'gmail.search',
|
||||||
|
'gmail.read',
|
||||||
|
'gmail.filter.create',
|
||||||
'minio.share',
|
'minio.share',
|
||||||
'minio.ingest',
|
'minio.ingest',
|
||||||
'minio.sync',
|
'minio.sync',
|
||||||
@@ -146,7 +150,7 @@ describe('ToolPolicy', () => {
|
|||||||
expect(names).not.toContain('mcp:filesystem:read_file');
|
expect(names).not.toContain('mcp:filesystem:read_file');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('messaging profile includes memory and web search', () => {
|
it('messaging profile includes memory, web search, and gmail tools', () => {
|
||||||
const policy = new ToolPolicy(defaultConfig({ profile: 'messaging' }));
|
const policy = new ToolPolicy(defaultConfig({ profile: 'messaging' }));
|
||||||
const result = policy.filterTools(ALL_TOOLS);
|
const result = policy.filterTools(ALL_TOOLS);
|
||||||
const names = result.map(t => t.name);
|
const names = result.map(t => t.name);
|
||||||
@@ -155,6 +159,10 @@ describe('ToolPolicy', () => {
|
|||||||
expect(names).toContain('memory.write');
|
expect(names).toContain('memory.write');
|
||||||
expect(names).toContain('web.search');
|
expect(names).toContain('web.search');
|
||||||
expect(names).toContain('web.search.news');
|
expect(names).toContain('web.search.news');
|
||||||
|
expect(names).toContain('gmail.list');
|
||||||
|
expect(names).toContain('gmail.search');
|
||||||
|
expect(names).toContain('gmail.read');
|
||||||
|
expect(names).toContain('gmail.filter.create');
|
||||||
expect(names).not.toContain('shell.exec');
|
expect(names).not.toContain('shell.exec');
|
||||||
expect(names).not.toContain('file.write');
|
expect(names).not.toContain('file.write');
|
||||||
});
|
});
|
||||||
@@ -503,6 +511,21 @@ describe('ToolPolicy', () => {
|
|||||||
expect(names).not.toContain('shell.exec');
|
expect(names).not.toContain('shell.exec');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('expands group:gmail', () => {
|
||||||
|
const policy = new ToolPolicy(defaultConfig({
|
||||||
|
profile: 'minimal',
|
||||||
|
allow: ['group:gmail'],
|
||||||
|
}));
|
||||||
|
const result = policy.filterTools(ALL_TOOLS);
|
||||||
|
const names = result.map(t => t.name);
|
||||||
|
expect(names).toContain('gmail.list');
|
||||||
|
expect(names).toContain('gmail.search');
|
||||||
|
expect(names).toContain('gmail.read');
|
||||||
|
expect(names).toContain('gmail.filter.create');
|
||||||
|
expect(names).toContain('file.read');
|
||||||
|
expect(names).not.toContain('shell.exec');
|
||||||
|
});
|
||||||
|
|
||||||
it('expands group:k8s', () => {
|
it('expands group:k8s', () => {
|
||||||
const policy = new ToolPolicy(defaultConfig({
|
const policy = new ToolPolicy(defaultConfig({
|
||||||
profile: 'minimal',
|
profile: 'minimal',
|
||||||
|
|||||||
+3
-1
@@ -25,6 +25,7 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
|||||||
'gmail.list',
|
'gmail.list',
|
||||||
'gmail.search',
|
'gmail.search',
|
||||||
'gmail.read',
|
'gmail.read',
|
||||||
|
'gmail.filter.create',
|
||||||
'calendar.today',
|
'calendar.today',
|
||||||
'calendar.list',
|
'calendar.list',
|
||||||
'calendar.search',
|
'calendar.search',
|
||||||
@@ -63,6 +64,7 @@ const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
|
|||||||
'gmail.list',
|
'gmail.list',
|
||||||
'gmail.search',
|
'gmail.search',
|
||||||
'gmail.read',
|
'gmail.read',
|
||||||
|
'gmail.filter.create',
|
||||||
'calendar.today',
|
'calendar.today',
|
||||||
'calendar.list',
|
'calendar.list',
|
||||||
'calendar.search',
|
'calendar.search',
|
||||||
@@ -117,7 +119,7 @@ export const TOOL_GROUPS: Record<string, string[]> = {
|
|||||||
'group:runtime': ['shell.exec', 'process.start', 'process.output', 'process.status', 'process.kill', 'process.list', 'screen.capture', 'camera.capture'],
|
'group:runtime': ['shell.exec', 'process.start', 'process.output', 'process.status', 'process.kill', 'process.list', 'screen.capture', 'camera.capture'],
|
||||||
'group:web': ['web.fetch', 'web.search', 'web.search.news', 'browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval', 'browser.evaluate'],
|
'group:web': ['web.fetch', 'web.search', 'web.search.news', 'browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval', 'browser.evaluate'],
|
||||||
'group:memory': ['memory.read', 'memory.write', 'memory.search'],
|
'group:memory': ['memory.read', 'memory.write', 'memory.search'],
|
||||||
'group:gmail': ['gmail.list', 'gmail.search', 'gmail.read'],
|
'group:gmail': ['gmail.list', 'gmail.search', 'gmail.read', 'gmail.filter.create'],
|
||||||
'group:gcal': ['calendar.today', 'calendar.list', 'calendar.search'],
|
'group:gcal': ['calendar.today', 'calendar.list', 'calendar.search'],
|
||||||
'group:gdocs': ['docs.list', 'docs.search', 'docs.read'],
|
'group:gdocs': ['docs.list', 'docs.search', 'docs.read'],
|
||||||
'group:gdrive': ['drive.list', 'drive.search', 'drive.read'],
|
'group:gdrive': ['drive.list', 'drive.search', 'drive.read'],
|
||||||
|
|||||||
Reference in New Issue
Block a user