Always allow

This commit is contained in:
andrepimenta
2025-07-08 22:29:57 +01:00
parent ede4fbaf98
commit 63acf5e7f9
4 changed files with 197 additions and 5 deletions

View File

@@ -272,7 +272,7 @@ class ClaudeChatProvider {
this._createImageFile(message.imageData, message.imageType);
return;
case 'permissionResponse':
this._handlePermissionResponse(message.id, message.approved);
this._handlePermissionResponse(message.id, message.approved, message.alwaysAllow);
return;
}
}
@@ -1109,16 +1109,85 @@ class ClaudeChatProvider {
});
}
private _handlePermissionResponse(id: string, approved: boolean): void {
private _handlePermissionResponse(id: string, approved: boolean, alwaysAllow?: boolean): void {
if (this._pendingPermissionResolvers && this._pendingPermissionResolvers.has(id)) {
const resolver = this._pendingPermissionResolvers.get(id);
if (resolver) {
resolver(approved);
this._pendingPermissionResolvers.delete(id);
// Handle always allow setting
if (alwaysAllow && approved) {
void this._saveAlwaysAllowPermission(id);
}
}
}
}
private async _saveAlwaysAllowPermission(requestId: string): Promise<void> {
try {
// Read the original request to get tool name and input
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) return;
const requestFileUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', `${requestId}.request`));
let requestContent: Uint8Array;
try {
requestContent = await vscode.workspace.fs.readFile(requestFileUri);
} catch {
return; // Request file doesn't exist
}
const request = JSON.parse(new TextDecoder().decode(requestContent));
// Load existing workspace permissions
const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json'));
let permissions: any = { alwaysAllow: {} };
try {
const content = await vscode.workspace.fs.readFile(permissionsUri);
permissions = JSON.parse(new TextDecoder().decode(content));
} catch {
// File doesn't exist yet, use default permissions
}
// Add the new permission
const toolName = request.tool;
if (toolName === 'Bash' && request.input?.command) {
// For Bash, store the specific command
if (!permissions.alwaysAllow[toolName]) {
permissions.alwaysAllow[toolName] = [];
}
if (Array.isArray(permissions.alwaysAllow[toolName])) {
const command = request.input.command.trim();
if (!permissions.alwaysAllow[toolName].includes(command)) {
permissions.alwaysAllow[toolName].push(command);
}
}
} else {
// For other tools, allow all instances
permissions.alwaysAllow[toolName] = true;
}
// Ensure permissions directory exists
const permissionsDir = vscode.Uri.file(path.dirname(permissionsUri.fsPath));
try {
await vscode.workspace.fs.stat(permissionsDir);
} catch {
await vscode.workspace.fs.createDirectory(permissionsDir);
}
// Save the permissions
const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2));
await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent);
console.log(`Saved always-allow permission for ${toolName}`);
} catch (error) {
console.error('Error saving always-allow permission:', error);
}
}
public getMCPConfigPath(): string | undefined {
const storagePath = this._context.storageUri?.fsPath;
if (!storagePath) {return undefined;}

View File

@@ -16,6 +16,61 @@ if (!PERMISSIONS_PATH) {
process.exit(1);
}
interface WorkspacePermissions {
alwaysAllow: {
[toolName: string]: boolean | string[]; // true for all, or array of allowed commands/patterns
};
}
function getWorkspacePermissionsPath(): string | null {
if (!PERMISSIONS_PATH) return null;
return path.join(PERMISSIONS_PATH, 'permissions.json');
}
function loadWorkspacePermissions(): WorkspacePermissions {
const permissionsPath = getWorkspacePermissionsPath();
if (!permissionsPath || !fs.existsSync(permissionsPath)) {
return { alwaysAllow: {} };
}
try {
const content = fs.readFileSync(permissionsPath, 'utf8');
return JSON.parse(content);
} catch (error) {
console.error(`Error loading workspace permissions: ${error}`);
return { alwaysAllow: {} };
}
}
function isAlwaysAllowed(toolName: string, input: any): boolean {
const permissions = loadWorkspacePermissions();
const toolPermission = permissions.alwaysAllow[toolName];
if (!toolPermission) return false;
// If it's true, always allow
if (toolPermission === true) return true;
// If it's an array, check for specific commands (mainly for Bash)
if (Array.isArray(toolPermission)) {
if (toolName === 'Bash' && input.command) {
const command = input.command.trim();
return toolPermission.some(allowedCmd => {
// Support exact match or pattern matching
if (allowedCmd.includes('*')) {
const pattern = allowedCmd.replace(/\*/g, '.*');
return new RegExp(`^${pattern}$`).test(command);
}
return command.startsWith(allowedCmd);
});
}
}
return false;
}
function generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
@@ -26,6 +81,12 @@ async function requestPermission(tool_name: string, input: any): Promise<{approv
return { approved: false, reason: "Permissions path not configured" };
}
// Check if this tool/command is always allowed for this workspace
if (isAlwaysAllowed(tool_name, input)) {
console.error(`Tool ${tool_name} is always allowed for this workspace`);
return { approved: true };
}
const requestId = generateRequestId();
const requestFile = path.join(PERMISSIONS_PATH, `${requestId}.request`);
const responseFile = path.join(PERMISSIONS_PATH, `${requestId}.response`);

View File

@@ -113,6 +113,45 @@ const styles = `
color: var(--vscode-descriptionForeground);
}
.permission-command {
margin: 8px 0;
padding: 8px;
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
font-family: var(--vscode-editor-font-family);
font-size: 12px;
}
.permission-command code {
color: var(--vscode-editor-foreground);
}
.permission-options {
margin: 12px 0;
padding: 8px 0;
border-top: 1px solid var(--vscode-panel-border);
}
.permission-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 12px;
}
.permission-checkbox input[type="checkbox"] {
margin: 0;
cursor: pointer;
}
.checkbox-label {
color: var(--vscode-foreground);
cursor: pointer;
line-height: 1.3;
}
.permission-tool {
font-family: var(--vscode-editor-font-family);
background-color: var(--vscode-editor-background);

View File

@@ -1973,6 +1973,10 @@ const html = `<!DOCTYPE html>
const toolName = data.tool || 'Unknown Tool';
// Create command display for Bash tool
const commandDisplay = toolName === 'Bash' && data.input?.command ?
\`<div class="permission-command"><code>\${data.input.command}</code></div>\` : '';
messageDiv.innerHTML = \`
<div class="permission-header">
<span class="icon">🔐</span>
@@ -1980,6 +1984,13 @@ const html = `<!DOCTYPE html>
</div>
<div class="permission-content">
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
\${commandDisplay}
<div class="permission-options">
<label class="permission-checkbox">
<input type="checkbox" id="always-allow-\${data.id}" class="always-allow-checkbox">
<span class="checkbox-label">Always allow \${toolName === 'Bash' ? 'this command' : 'this tool'} in this workspace</span>
</label>
</div>
<div class="permission-buttons">
<button class="btn allow" onclick="respondToPermission('\${data.id}', true)">Allow</button>
<button class="btn deny" onclick="respondToPermission('\${data.id}', false)">Deny</button>
@@ -1992,11 +2003,16 @@ const html = `<!DOCTYPE html>
}
function respondToPermission(id, approved) {
// Check if always-allow is checked
const alwaysAllowCheckbox = document.getElementById(\`always-allow-\${id}\`);
const alwaysAllow = alwaysAllowCheckbox?.checked || false;
// Send response back to extension
vscode.postMessage({
type: 'permissionResponse',
id: id,
approved: approved
approved: approved,
alwaysAllow: alwaysAllow
});
// Update the UI to show the decision
@@ -2004,12 +2020,19 @@ const html = `<!DOCTYPE html>
if (permissionMsg) {
const buttons = permissionMsg.querySelector('.permission-buttons');
const permissionContent = permissionMsg.querySelector('.permission-content');
const decision = approved ? 'You allowed this' : 'You denied this';
const options = permissionMsg.querySelector('.permission-options');
let decision = approved ? 'You allowed this' : 'You denied this';
if (alwaysAllow && approved) {
decision = 'You allowed this and set it to always allow';
}
const emoji = approved ? '✅' : '❌';
const decisionClass = approved ? 'allowed' : 'denied';
// Hide buttons
// Hide buttons and options
buttons.style.display = 'none';
options.style.display = 'none';
// Add decision div to permission-content
const decisionDiv = document.createElement('div');