diff --git a/package-lock.json b/package-lock.json index 4d4e6dd..fe464f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "hermes-ai-agent", - "version": "3.0.0", + "name": "hermes-code-agent", + "version": "3.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "hermes-ai-agent", - "version": "3.0.0", + "name": "hermes-code-agent", + "version": "3.0.2", "license": "MIT", "dependencies": { "dompurify": "^3.4.5", diff --git a/package.json b/package.json index 50ffbf2..365c952 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "hermes-ai-agent", - "displayName": "Hermes AI Agent", - "description": "VS Code sidebar for the Hermes AI agent. Streams chat, runs tools, manages sessions. Multi-model (Claude, Codex). Communicates over ACP.", - "version": "3.0.1", + "name": "hermes-code-agent", + "displayName": "Hermes Code Agent", + "description": "VS Code sidebar for the Hermes Code Agent. Streams chat, runs tools, manages sessions. Multi-model (Claude, Codex). Communicates over ACP.", + "version": "3.0.2", "publisher": "gitricko", "author": "gitricko", "license": "MIT", @@ -17,7 +17,7 @@ "llm", "tool use", "chat", - "hermes", + "hermes agent", "acp", "agent client protocol", "sidebar chat", @@ -67,7 +67,7 @@ "activitybar": [ { "id": "hermes", - "title": "Hermes Agent", + "title": "Hermes Code Agent", "icon": "resources/hermes-icon.svg" } ] diff --git a/src/extension.ts b/src/extension.ts index 99c67f8..9acbda9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,6 +10,37 @@ import { ChatPanelProvider } from './chatPanel'; const DEFAULT_SONNET_MODEL = 'claude-sonnet-4-6'; const APPROVED_BINARIES_KEY = 'hermes.approvedBinaries'; +const WHITELISTED_TOOLS_KEY = 'hermes.whitelistedTools'; + +function readApprovalMode(): boolean { + try { + const configPath = path.join(os.homedir(), '.hermes', 'config.yaml'); + const content = fs.readFileSync(configPath, 'utf8'); + const lines = content.split(/\r?\n/); + + for (let i = 0; i < lines.length; i += 1) { + const approvalsMatch = /^(\s*)approvals:\s*$/.exec(lines[i]); + if (!approvalsMatch) continue; + + const approvalsIndent = approvalsMatch[1].length; + for (let j = i + 1; j < lines.length; j += 1) { + const line = lines[j]; + if (!line.trim() || line.trimStart().startsWith('#')) continue; + + const lineIndent = line.match(/^\s*/)?.[0].length ?? 0; + if (lineIndent <= approvalsIndent) break; + + const modeMatch = /^\s*mode:\s*([^\s#]+)/.exec(line); + if (modeMatch) { + const rawMode = modeMatch[1].trim().toLowerCase(); + const mode = rawMode.replace(/^['"]|['"]$/g, ''); + return !['off', 'false', 'disabled', 'none', 'auto', 'yolo'].includes(mode); + } + } + } + } catch { } + return true; // Default to safe mode if config missing/unparseable +} function extractModelFromHermesConfig(content: string): string | null { const lines = content.split(/\r?\n/); @@ -235,16 +266,46 @@ export async function activate(context: vscode.ExtensionContext): Promise setStatus('disconnected'); }); + const approvalsRequired = readApprovalMode(); + const permissionHandler: PermissionRequestHandler = async (_method, params) => { + const toolName = (() => { + if (!params || typeof params !== 'object') return 'an action'; + const record = params as Record; + const raw = typeof record.toolName === 'string' + ? record.toolName + : typeof record.title === 'string' + ? record.title + : typeof record.kind === 'string' + ? record.kind + : 'an action'; + const cleaned = raw.replace(/[\r\n]+/g, ' ').trim(); + return cleaned || 'an action'; + })(); + const whitelisted = context.workspaceState.get(WHITELISTED_TOOLS_KEY, []); + if (!approvalsRequired || whitelisted.includes(toolName)) { + const allowOptionId = optionIdByIntent(params, 'allow'); + if (allowOptionId) { + return { outcome: 'selected', optionId: allowOptionId }; + } + } + const allowOptionId = optionIdByIntent(params, 'allow'); const denyOptionId = optionIdByIntent(params, 'deny'); const allow = 'Allow Once'; const deny = 'Deny'; + const always = 'Always Allow'; + const buttons: string[] = []; + if (denyOptionId) { + buttons.push(deny); + } + if (allowOptionId) { + buttons.push(allow, always); + } const choice = await vscode.window.showWarningMessage( - summarizePermissionRequest(params), + summarizePermissionRequest(params).replace(/[\r\n]+/g, ' '), { modal: true }, - allow, - deny, + ...buttons, ); if (choice === allow && allowOptionId) { @@ -252,6 +313,16 @@ export async function activate(context: vscode.ExtensionContext): Promise return { outcome: 'selected', optionId: allowOptionId }; } + if (choice === always && allowOptionId) { + if (toolName === 'an action') { + outputChannel.appendLine('[security] refusing to whitelist fallback tool identifier "an action"'); + } else { + outputChannel.appendLine(`[security] tool whitelisted: ${toolName}`); + await context.workspaceState.update(WHITELISTED_TOOLS_KEY, [...new Set([...whitelisted, toolName])]); + } + return { outcome: 'selected', optionId: allowOptionId }; + } + if (denyOptionId) { outputChannel.appendLine('[security] permission denied'); return { outcome: 'selected', optionId: denyOptionId };