mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-07 12:18:44 +02:00
feat: add tool-specific approval overrides
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -30,6 +30,9 @@ import {
|
||||
enqueuePendingApprovalState,
|
||||
listPendingApprovals,
|
||||
normalizeApprovalPolicy,
|
||||
normalizeSessionApprovalSettings,
|
||||
pruneApprovalPolicyTools,
|
||||
pruneSessionApprovalSettings,
|
||||
resolvePendingApproval,
|
||||
type ApprovalDecision,
|
||||
type PendingApprovalMessageRecord,
|
||||
@@ -45,6 +48,7 @@ import {
|
||||
} from '@shared/domain/sessionLibrary';
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import {
|
||||
applySessionApprovalSettings,
|
||||
applyScratchpadSessionConfig,
|
||||
resolveSessionToolingSelection,
|
||||
createScratchpadSessionConfig,
|
||||
@@ -64,6 +68,7 @@ import {
|
||||
} from '@shared/domain/runTimeline';
|
||||
import {
|
||||
createSessionToolingSelection,
|
||||
listApprovalToolNames,
|
||||
normalizeTheme,
|
||||
type AppearanceTheme,
|
||||
type LspProfileDefinition,
|
||||
@@ -195,7 +200,10 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
|
||||
async savePattern(pattern: PatternDefinition): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const issues = validatePatternDefinition(pattern).filter((issue) => issue.level === 'error');
|
||||
const issues = validatePatternDefinition(
|
||||
pattern,
|
||||
this.listKnownApprovalToolNames(workspace),
|
||||
).filter((issue) => issue.level === 'error');
|
||||
if (issues.length > 0) {
|
||||
throw new Error(issues[0].message);
|
||||
}
|
||||
@@ -273,6 +281,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
workspace.settings.tooling.mcpServers.push(candidate);
|
||||
}
|
||||
|
||||
this.pruneUnavailableApprovalTools(workspace);
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
@@ -290,6 +299,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
};
|
||||
}
|
||||
|
||||
this.pruneUnavailableApprovalTools(workspace);
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
@@ -318,6 +328,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
workspace.settings.tooling.lspProfiles.push(candidate);
|
||||
}
|
||||
|
||||
this.pruneUnavailableApprovalTools(workspace);
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
@@ -335,6 +346,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
};
|
||||
}
|
||||
|
||||
this.pruneUnavailableApprovalTools(workspace);
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
@@ -667,6 +679,41 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async updateSessionApprovalSettings(
|
||||
sessionId: string,
|
||||
autoApprovedToolNames?: string[],
|
||||
): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
const project = this.requireProject(workspace, session.projectId);
|
||||
|
||||
if (session.status === 'running') {
|
||||
throw new Error('Wait for the current response to finish before changing session approval settings.');
|
||||
}
|
||||
|
||||
const settings = normalizeSessionApprovalSettings(
|
||||
autoApprovedToolNames === undefined ? undefined : { autoApprovedToolNames },
|
||||
);
|
||||
|
||||
if (
|
||||
isScratchpadProject(project)
|
||||
&& settings
|
||||
&& settings.autoApprovedToolNames.length > 0
|
||||
) {
|
||||
throw new Error('Scratchpad sessions do not support tool auto-approval settings.');
|
||||
}
|
||||
|
||||
const knownToolNames = new Set(this.listKnownApprovalToolNames(workspace));
|
||||
const unknownToolName = settings?.autoApprovedToolNames.find((toolName) => !knownToolNames.has(toolName));
|
||||
if (unknownToolName) {
|
||||
throw new Error(`Unknown approval tool "${unknownToolName}".`);
|
||||
}
|
||||
|
||||
session.approvalSettings = settings;
|
||||
session.updatedAt = nowIso();
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async querySessions(input: QuerySessionsInput): Promise<SessionQueryResult[]> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
return queryWorkspaceSessions(workspace, input);
|
||||
@@ -1119,9 +1166,29 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
const patternWithSessionConfig = isScratchpadProject(project)
|
||||
? applyScratchpadSessionConfig(pattern, session)
|
||||
: pattern;
|
||||
const patternWithApprovalSettings = applySessionApprovalSettings(patternWithSessionConfig, session);
|
||||
|
||||
const modelCatalog = await this.loadAvailableModelCatalog();
|
||||
return normalizePatternModels(patternWithSessionConfig, modelCatalog);
|
||||
return normalizePatternModels(patternWithApprovalSettings, modelCatalog);
|
||||
}
|
||||
|
||||
private listKnownApprovalToolNames(workspace: WorkspaceState): string[] {
|
||||
return listApprovalToolNames(workspace.settings.tooling);
|
||||
}
|
||||
|
||||
private pruneUnavailableApprovalTools(workspace: WorkspaceState): void {
|
||||
const knownToolNames = this.listKnownApprovalToolNames(workspace);
|
||||
|
||||
for (const pattern of workspace.patterns) {
|
||||
pattern.approvalPolicy = pruneApprovalPolicyTools(pattern.approvalPolicy, knownToolNames);
|
||||
}
|
||||
|
||||
for (const session of workspace.sessions) {
|
||||
session.approvalSettings = pruneSessionApprovalSettings(
|
||||
session.approvalSettings,
|
||||
knownToolNames,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private buildRunTurnToolingConfig(
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
SetPatternFavoriteInput,
|
||||
SetSessionArchivedInput,
|
||||
SetSessionPinnedInput,
|
||||
UpdateSessionApprovalSettingsInput,
|
||||
UpdateSessionToolingInput,
|
||||
UpdateScratchpadSessionConfigInput,
|
||||
} from '@shared/contracts/ipc';
|
||||
@@ -60,6 +61,11 @@ export function registerIpcHandlers(window: BrowserWindow, service: EryxAppServi
|
||||
input.enabledLspProfileIds,
|
||||
),
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcChannels.updateSessionApprovalSettings,
|
||||
(_event, input: UpdateSessionApprovalSettingsInput) =>
|
||||
service.updateSessionApprovalSettings(input.sessionId, input.autoApprovedToolNames),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.createSession, (_event, input: CreateSessionInput) =>
|
||||
service.createSession(input.projectId, input.patternId),
|
||||
);
|
||||
|
||||
@@ -4,10 +4,17 @@ import { createBuiltinPatterns } from '@shared/domain/pattern';
|
||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||
import { mergeScratchpadProject } from '@shared/domain/project';
|
||||
import { normalizeSessionRunRecords } from '@shared/domain/runTimeline';
|
||||
import { normalizeSessionToolingSelection, normalizeWorkspaceSettings } from '@shared/domain/tooling';
|
||||
import {
|
||||
listApprovalToolNames,
|
||||
normalizeSessionToolingSelection,
|
||||
normalizeWorkspaceSettings,
|
||||
} from '@shared/domain/tooling';
|
||||
import {
|
||||
normalizeApprovalPolicy,
|
||||
normalizePendingApprovalState,
|
||||
normalizeSessionApprovalSettings,
|
||||
pruneApprovalPolicyTools,
|
||||
pruneSessionApprovalSettings,
|
||||
} from '@shared/domain/approval';
|
||||
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||
import { nowIso } from '@shared/utils/ids';
|
||||
@@ -60,24 +67,33 @@ export class WorkspaceRepository {
|
||||
}
|
||||
|
||||
const projects = mergeScratchpadProject(stored.projects ?? [], this.scratchpadPath);
|
||||
const settings = normalizeWorkspaceSettings(stored.settings);
|
||||
const knownToolNames = listApprovalToolNames(settings.tooling);
|
||||
|
||||
const workspace: WorkspaceState = {
|
||||
...stored,
|
||||
patterns: mergePatterns(stored.patterns ?? []).map((pattern) => ({
|
||||
...pattern,
|
||||
approvalPolicy: normalizeApprovalPolicy(pattern.approvalPolicy),
|
||||
approvalPolicy: pruneApprovalPolicyTools(
|
||||
normalizeApprovalPolicy(pattern.approvalPolicy),
|
||||
knownToolNames,
|
||||
),
|
||||
})),
|
||||
projects,
|
||||
sessions: (stored.sessions ?? []).map((session) => ({
|
||||
...session,
|
||||
runs: normalizeSessionRunRecords(session.runs),
|
||||
tooling: normalizeSessionToolingSelection(session.tooling),
|
||||
approvalSettings: pruneSessionApprovalSettings(
|
||||
normalizeSessionApprovalSettings(session.approvalSettings),
|
||||
knownToolNames,
|
||||
),
|
||||
...normalizePendingApprovalState({
|
||||
pendingApproval: session.pendingApproval,
|
||||
pendingApprovalQueue: session.pendingApprovalQueue,
|
||||
}),
|
||||
})),
|
||||
settings: normalizeWorkspaceSettings(stored.settings),
|
||||
settings,
|
||||
selectedProjectId: projects.some((project) => project.id === stored.selectedProjectId)
|
||||
? stored.selectedProjectId
|
||||
: projects[0]?.id,
|
||||
|
||||
@@ -19,6 +19,8 @@ const api: ElectronApi = {
|
||||
saveLspProfile: (input) => ipcRenderer.invoke(ipcChannels.saveLspProfile, input),
|
||||
deleteLspProfile: (profileId) => ipcRenderer.invoke(ipcChannels.deleteLspProfile, profileId),
|
||||
updateSessionTooling: (input) => ipcRenderer.invoke(ipcChannels.updateSessionTooling, input),
|
||||
updateSessionApprovalSettings: (input) =>
|
||||
ipcRenderer.invoke(ipcChannels.updateSessionApprovalSettings, input),
|
||||
createSession: (input) => ipcRenderer.invoke(ipcChannels.createSession, input),
|
||||
duplicateSession: (input) => ipcRenderer.invoke(ipcChannels.duplicateSession, input),
|
||||
renameSession: (input) => ipcRenderer.invoke(ipcChannels.renameSession, input),
|
||||
|
||||
@@ -14,6 +14,7 @@ export const ipcChannels = {
|
||||
saveLspProfile: 'tooling:lsp:save',
|
||||
deleteLspProfile: 'tooling:lsp:delete',
|
||||
updateSessionTooling: 'sessions:update-tooling',
|
||||
updateSessionApprovalSettings: 'sessions:update-approval-settings',
|
||||
createSession: 'sessions:create',
|
||||
duplicateSession: 'sessions:duplicate',
|
||||
renameSession: 'sessions:rename',
|
||||
|
||||
@@ -74,6 +74,11 @@ export interface UpdateSessionToolingInput extends SessionToolingSelection {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface UpdateSessionApprovalSettingsInput {
|
||||
sessionId: string;
|
||||
autoApprovedToolNames?: string[];
|
||||
}
|
||||
|
||||
export interface ElectronApi {
|
||||
describeSidecarCapabilities(): Promise<SidecarCapabilities>;
|
||||
refreshSidecarCapabilities(): Promise<SidecarCapabilities>;
|
||||
@@ -88,6 +93,7 @@ export interface ElectronApi {
|
||||
saveLspProfile(input: SaveLspProfileInput): Promise<WorkspaceState>;
|
||||
deleteLspProfile(profileId: string): Promise<WorkspaceState>;
|
||||
updateSessionTooling(input: UpdateSessionToolingInput): Promise<WorkspaceState>;
|
||||
updateSessionApprovalSettings(input: UpdateSessionApprovalSettingsInput): Promise<WorkspaceState>;
|
||||
createSession(input: CreateSessionInput): Promise<WorkspaceState>;
|
||||
duplicateSession(input: DuplicateSessionInput): Promise<WorkspaceState>;
|
||||
renameSession(input: RenameSessionInput): Promise<WorkspaceState>;
|
||||
|
||||
@@ -9,6 +9,11 @@ export interface ApprovalCheckpointRule {
|
||||
|
||||
export interface ApprovalPolicy {
|
||||
rules: ApprovalCheckpointRule[];
|
||||
autoApprovedToolNames?: string[];
|
||||
}
|
||||
|
||||
export interface SessionApprovalSettings {
|
||||
autoApprovedToolNames: string[];
|
||||
}
|
||||
|
||||
export interface PendingApprovalMessageRecord {
|
||||
@@ -53,6 +58,7 @@ export function normalizeApprovalPolicy(policy?: Partial<ApprovalPolicy>): Appro
|
||||
const rules = Array.isArray(policy?.rules) ? policy.rules : [];
|
||||
const selectedAgents = new Map<ApprovalCheckpointKind, Set<string>>();
|
||||
const appliesToAllAgents = new Set<ApprovalCheckpointKind>();
|
||||
const autoApprovedToolNames = normalizeStringArray(policy?.autoApprovedToolNames);
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!isApprovalCheckpointKind(rule?.kind)) {
|
||||
@@ -90,18 +96,39 @@ export function normalizeApprovalPolicy(policy?: Partial<ApprovalPolicy>): Appro
|
||||
return [{ kind, agentIds }];
|
||||
});
|
||||
|
||||
return normalizedRules.length > 0 ? { rules: normalizedRules } : undefined;
|
||||
if (normalizedRules.length === 0 && autoApprovedToolNames.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
rules: normalizedRules,
|
||||
autoApprovedToolNames: autoApprovedToolNames.length > 0 ? autoApprovedToolNames : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeSessionApprovalSettings(
|
||||
settings?: Partial<SessionApprovalSettings>,
|
||||
): SessionApprovalSettings | undefined {
|
||||
if (settings == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
autoApprovedToolNames: normalizeStringArray(settings.autoApprovedToolNames),
|
||||
};
|
||||
}
|
||||
|
||||
export function validateApprovalPolicy(
|
||||
policy: ApprovalPolicy | undefined,
|
||||
knownAgentIds: readonly string[],
|
||||
knownToolNames?: readonly string[],
|
||||
): string[] {
|
||||
if (!policy) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const knownAgents = new Set(normalizeStringArray(knownAgentIds));
|
||||
const knownTools = knownToolNames ? new Set(normalizeStringArray(knownToolNames)) : undefined;
|
||||
const issues: string[] = [];
|
||||
|
||||
for (const rule of policy.rules) {
|
||||
@@ -112,6 +139,14 @@ export function validateApprovalPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
if (knownTools) {
|
||||
for (const toolName of policy.autoApprovedToolNames ?? []) {
|
||||
if (!knownTools.has(toolName)) {
|
||||
issues.push(`Approval auto-approve references unknown tool "${toolName}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
@@ -137,6 +172,81 @@ export function approvalPolicyRequiresCheckpoint(
|
||||
return rule.agentIds.includes(normalizedAgentId);
|
||||
}
|
||||
|
||||
export function approvalPolicyAutoApprovesTool(
|
||||
policy: ApprovalPolicy | undefined,
|
||||
toolName?: string,
|
||||
): boolean {
|
||||
const normalizedToolName = normalizeOptionalString(toolName);
|
||||
if (!normalizedToolName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return policy?.autoApprovedToolNames?.includes(normalizedToolName) ?? false;
|
||||
}
|
||||
|
||||
export function approvalPolicyRequiresToolCallApproval(
|
||||
policy: ApprovalPolicy | undefined,
|
||||
agentId?: string,
|
||||
toolName?: string,
|
||||
): boolean {
|
||||
if (!approvalPolicyRequiresCheckpoint(policy, 'tool-call', agentId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !approvalPolicyAutoApprovesTool(policy, toolName);
|
||||
}
|
||||
|
||||
export function resolveEffectiveApprovalPolicy(
|
||||
policy: ApprovalPolicy | undefined,
|
||||
sessionSettings?: Partial<SessionApprovalSettings>,
|
||||
): ApprovalPolicy | undefined {
|
||||
if (sessionSettings === undefined) {
|
||||
return normalizeApprovalPolicy(policy);
|
||||
}
|
||||
|
||||
const normalizedPolicy = normalizeApprovalPolicy(policy);
|
||||
const normalizedSessionSettings = normalizeSessionApprovalSettings(sessionSettings);
|
||||
return normalizeApprovalPolicy({
|
||||
rules: normalizedPolicy?.rules ?? [],
|
||||
autoApprovedToolNames: normalizedSessionSettings?.autoApprovedToolNames,
|
||||
});
|
||||
}
|
||||
|
||||
export function pruneApprovalPolicyTools(
|
||||
policy: ApprovalPolicy | undefined,
|
||||
knownToolNames: readonly string[],
|
||||
): ApprovalPolicy | undefined {
|
||||
const normalizedPolicy = normalizeApprovalPolicy(policy);
|
||||
if (!normalizedPolicy) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return normalizeApprovalPolicy({
|
||||
...normalizedPolicy,
|
||||
autoApprovedToolNames: filterKnownToolNames(
|
||||
normalizedPolicy.autoApprovedToolNames,
|
||||
knownToolNames,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function pruneSessionApprovalSettings(
|
||||
settings: SessionApprovalSettings | undefined,
|
||||
knownToolNames: readonly string[],
|
||||
): SessionApprovalSettings | undefined {
|
||||
const normalizedSettings = normalizeSessionApprovalSettings(settings);
|
||||
if (normalizedSettings === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
autoApprovedToolNames: filterKnownToolNames(
|
||||
normalizedSettings.autoApprovedToolNames,
|
||||
knownToolNames,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizePendingApproval(
|
||||
approval?: Partial<PendingApprovalRecord>,
|
||||
): PendingApprovalRecord | undefined {
|
||||
@@ -283,3 +393,11 @@ function normalizeStringArray(values?: ReadonlyArray<string>): string[] {
|
||||
|
||||
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
|
||||
}
|
||||
|
||||
function filterKnownToolNames(
|
||||
toolNames: readonly string[] | undefined,
|
||||
knownToolNames: readonly string[],
|
||||
): string[] {
|
||||
const knownTools = new Set(normalizeStringArray(knownToolNames));
|
||||
return normalizeStringArray(toolNames).filter((toolName) => knownTools.has(toolName));
|
||||
}
|
||||
|
||||
@@ -258,7 +258,10 @@ export function createBuiltinPatterns(timestamp: string): PatternDefinition[] {
|
||||
];
|
||||
}
|
||||
|
||||
export function validatePatternDefinition(pattern: PatternDefinition): PatternValidationIssue[] {
|
||||
export function validatePatternDefinition(
|
||||
pattern: PatternDefinition,
|
||||
knownToolNames?: readonly string[],
|
||||
): PatternValidationIssue[] {
|
||||
const issues: PatternValidationIssue[] = [];
|
||||
|
||||
if (!pattern.name.trim()) {
|
||||
@@ -324,6 +327,7 @@ export function validatePatternDefinition(pattern: PatternDefinition): PatternVa
|
||||
for (const message of validateApprovalPolicy(
|
||||
normalizeApprovalPolicy(pattern.approvalPolicy),
|
||||
pattern.agents.map((agent) => agent.id),
|
||||
knownToolNames,
|
||||
)) {
|
||||
issues.push({
|
||||
level: 'error',
|
||||
|
||||
@@ -4,7 +4,12 @@ import {
|
||||
normalizeSessionToolingSelection,
|
||||
type SessionToolingSelection,
|
||||
} from '@shared/domain/tooling';
|
||||
import type { PendingApprovalRecord } from '@shared/domain/approval';
|
||||
import {
|
||||
normalizeSessionApprovalSettings,
|
||||
resolveEffectiveApprovalPolicy,
|
||||
type PendingApprovalRecord,
|
||||
type SessionApprovalSettings,
|
||||
} from '@shared/domain/approval';
|
||||
import type { SessionRunRecord } from '@shared/domain/runTimeline';
|
||||
|
||||
export type ChatRole = 'system' | 'user' | 'assistant';
|
||||
@@ -40,6 +45,7 @@ export interface SessionRecord {
|
||||
lastError?: string;
|
||||
scratchpadConfig?: ScratchpadSessionConfig;
|
||||
tooling?: SessionToolingSelection;
|
||||
approvalSettings?: SessionApprovalSettings;
|
||||
pendingApproval?: PendingApprovalRecord;
|
||||
pendingApprovalQueue?: PendingApprovalRecord[];
|
||||
runs: SessionRunRecord[];
|
||||
@@ -77,6 +83,12 @@ export function resolveSessionToolingSelection(
|
||||
return normalizeSessionToolingSelection(session.tooling ?? createSessionToolingSelection());
|
||||
}
|
||||
|
||||
export function resolveSessionApprovalSettings(
|
||||
session: Pick<SessionRecord, 'approvalSettings'>,
|
||||
): SessionApprovalSettings | undefined {
|
||||
return normalizeSessionApprovalSettings(session.approvalSettings);
|
||||
}
|
||||
|
||||
export function resolveScratchpadSessionConfig(
|
||||
session: SessionRecord,
|
||||
pattern: PatternDefinition,
|
||||
@@ -115,3 +127,17 @@ export function applyScratchpadSessionConfig(
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function applySessionApprovalSettings(
|
||||
pattern: PatternDefinition,
|
||||
session: Pick<SessionRecord, 'approvalSettings'>,
|
||||
): PatternDefinition {
|
||||
if (session.approvalSettings === undefined) {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
return {
|
||||
...pattern,
|
||||
approvalPolicy: resolveEffectiveApprovalPolicy(pattern.approvalPolicy, session.approvalSettings),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -181,6 +181,11 @@ export function duplicateSessionRecord(
|
||||
enabledLspProfileIds: [...session.tooling.enabledLspProfileIds],
|
||||
}
|
||||
: undefined,
|
||||
approvalSettings: session.approvalSettings
|
||||
? {
|
||||
autoApprovedToolNames: [...session.approvalSettings.autoApprovedToolNames],
|
||||
}
|
||||
: undefined,
|
||||
pendingApproval: undefined,
|
||||
pendingApprovalQueue: undefined,
|
||||
runs: [],
|
||||
|
||||
@@ -52,6 +52,24 @@ export interface SessionToolingSelection {
|
||||
enabledLspProfileIds: string[];
|
||||
}
|
||||
|
||||
export type ApprovalToolKind = 'mcp' | 'lsp' | 'mixed';
|
||||
|
||||
export interface ApprovalToolDefinition {
|
||||
id: string;
|
||||
label: string;
|
||||
kind: ApprovalToolKind;
|
||||
providerIds: string[];
|
||||
providerNames: string[];
|
||||
}
|
||||
|
||||
const lspApprovalOperations = [
|
||||
{ suffix: 'workspace_symbols', label: 'Workspace symbols' },
|
||||
{ suffix: 'document_symbols', label: 'Document symbols' },
|
||||
{ suffix: 'definition', label: 'Definition' },
|
||||
{ suffix: 'hover', label: 'Hover' },
|
||||
{ suffix: 'references', label: 'References' },
|
||||
] as const;
|
||||
|
||||
export function createWorkspaceSettings(): WorkspaceSettings {
|
||||
return {
|
||||
theme: 'dark',
|
||||
@@ -94,6 +112,44 @@ export function normalizeSessionToolingSelection(
|
||||
};
|
||||
}
|
||||
|
||||
export function listApprovalToolDefinitions(
|
||||
tooling: WorkspaceToolingSettings,
|
||||
): ApprovalToolDefinition[] {
|
||||
const toolsById = new Map<string, ApprovalToolDefinition>();
|
||||
|
||||
for (const server of tooling.mcpServers) {
|
||||
for (const toolName of normalizeStringArray(server.tools)) {
|
||||
registerApprovalTool(toolsById, {
|
||||
id: toolName,
|
||||
label: toolName,
|
||||
kind: 'mcp',
|
||||
providerId: server.id,
|
||||
providerName: server.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const profile of tooling.lspProfiles) {
|
||||
const toolPrefix = buildLspApprovalToolPrefix(profile.id);
|
||||
for (const operation of lspApprovalOperations) {
|
||||
registerApprovalTool(toolsById, {
|
||||
id: `${toolPrefix}_${operation.suffix}`,
|
||||
label: `${profile.name} · ${operation.label}`,
|
||||
kind: 'lsp',
|
||||
providerId: profile.id,
|
||||
providerName: profile.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...toolsById.values()].sort((left, right) =>
|
||||
left.label.localeCompare(right.label) || left.id.localeCompare(right.id));
|
||||
}
|
||||
|
||||
export function listApprovalToolNames(tooling: WorkspaceToolingSettings): string[] {
|
||||
return listApprovalToolDefinitions(tooling).map((tool) => tool.id);
|
||||
}
|
||||
|
||||
export function validateMcpServerDefinition(server: McpServerDefinition): string | undefined {
|
||||
if (!server.name.trim()) {
|
||||
return 'MCP server name is required.';
|
||||
@@ -196,6 +252,58 @@ function requiresTypeScriptLanguageServerStdio(command: string): boolean {
|
||||
|| executableName === 'typescript-language-server.exe';
|
||||
}
|
||||
|
||||
function buildLspApprovalToolPrefix(value: string): string {
|
||||
let prefix = '';
|
||||
for (const char of value) {
|
||||
if (/[a-z0-9]/i.test(char)) {
|
||||
prefix += char.toLowerCase();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!prefix || prefix.endsWith('_')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
prefix += '_';
|
||||
}
|
||||
|
||||
const normalized = prefix.replace(/^_+|_+$/g, '');
|
||||
return normalized ? `lsp_${normalized}` : 'lsp';
|
||||
}
|
||||
|
||||
function registerApprovalTool(
|
||||
toolsById: Map<string, ApprovalToolDefinition>,
|
||||
tool: {
|
||||
id: string;
|
||||
label: string;
|
||||
kind: Exclude<ApprovalToolKind, 'mixed'>;
|
||||
providerId: string;
|
||||
providerName: string;
|
||||
},
|
||||
): void {
|
||||
const existing = toolsById.get(tool.id);
|
||||
if (!existing) {
|
||||
toolsById.set(tool.id, {
|
||||
id: tool.id,
|
||||
label: tool.label,
|
||||
kind: tool.kind,
|
||||
providerIds: [tool.providerId],
|
||||
providerNames: [tool.providerName],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existing.providerIds.includes(tool.providerId)) {
|
||||
existing.providerIds.push(tool.providerId);
|
||||
}
|
||||
if (!existing.providerNames.includes(tool.providerName)) {
|
||||
existing.providerNames.push(tool.providerName);
|
||||
}
|
||||
if (existing.kind !== tool.kind) {
|
||||
existing.kind = 'mixed';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStringArray(values?: ReadonlyArray<string>): string[] {
|
||||
if (!values) {
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user