From 08c91010221abc8f6853e871cb0afccbaaf15227 Mon Sep 17 00:00:00 2001 From: "intricko@github" Date: Sun, 24 May 2026 23:16:36 -0400 Subject: [PATCH 01/12] Refactor package metadata and implement tool approval mechanism with whitelisting options --- package-lock.json | 8 ++++---- package.json | 12 ++++++------ src/extension.ts | 31 +++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 10 deletions(-) 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..767cfd1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,6 +10,19 @@ 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 match = /approvals:\s*\n\s*mode:\s*(\w+)/.exec(content); + if (match) { + return match[1].trim() === 'true'; + } + } catch { } + return true; // Default to safe mode if config missing/unparseable +} function extractModelFromHermesConfig(content: string): string | null { const lines = content.split(/\r?\n/); @@ -236,14 +249,26 @@ export async function activate(context: vscode.ExtensionContext): Promise }); const permissionHandler: PermissionRequestHandler = async (_method, params) => { + const toolName = summarizePermissionRequest(params); + const whitelisted = context.workspaceState.get(WHITELISTED_TOOLS_KEY, []); + + if (!readApprovalMode() || 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 always = 'Always Allow'; const deny = 'Deny'; const choice = await vscode.window.showWarningMessage( summarizePermissionRequest(params), { modal: true }, allow, + always, deny, ); @@ -252,6 +277,12 @@ export async function activate(context: vscode.ExtensionContext): Promise return { outcome: 'selected', optionId: allowOptionId }; } + if (choice === always && allowOptionId) { + 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 }; From 6d75b3bca5c74f28c104138a9ef86eedd43c1bef Mon Sep 17 00:00:00 2001 From: gitricko Date: Sun, 24 May 2026 23:32:01 -0400 Subject: [PATCH 02/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/extension.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 767cfd1..e609027 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -248,11 +248,23 @@ export async function activate(context: vscode.ExtensionContext): Promise setStatus('disconnected'); }); + const approvalsRequired = readApprovalMode(); + const permissionHandler: PermissionRequestHandler = async (_method, params) => { - const toolName = summarizePermissionRequest(params); + const toolName = (() => { + if (!params || typeof params !== 'object') return 'an action'; + const record = params as Record; + return typeof record.toolName === 'string' + ? record.toolName + : typeof record.title === 'string' + ? record.title + : typeof record.kind === 'string' + ? record.kind + : 'an action'; + })(); const whitelisted = context.workspaceState.get(WHITELISTED_TOOLS_KEY, []); - if (!readApprovalMode() || whitelisted.includes(toolName)) { + if (!approvalsRequired || whitelisted.includes(toolName)) { const allowOptionId = optionIdByIntent(params, 'allow'); if (allowOptionId) { return { outcome: 'selected', optionId: allowOptionId }; From 7fd09926d4e7cad4e1bab238ee8ebfc14c1c1025 Mon Sep 17 00:00:00 2001 From: "intricko@github" Date: Sun, 24 May 2026 23:59:40 -0400 Subject: [PATCH 03/12] dev --- src/extension.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index e609027..356f1f6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,7 +16,7 @@ function readApprovalMode(): boolean { try { const configPath = path.join(os.homedir(), '.hermes', 'config.yaml'); const content = fs.readFileSync(configPath, 'utf8'); - const match = /approvals:\s*\n\s*mode:\s*(\w+)/.exec(content); + const match = /approvals:[\s\S]*?mode:\s*(\w+)/.exec(content); if (match) { return match[1].trim() === 'true'; } @@ -274,14 +274,14 @@ export async function activate(context: vscode.ExtensionContext): Promise const allowOptionId = optionIdByIntent(params, 'allow'); const denyOptionId = optionIdByIntent(params, 'deny'); const allow = 'Allow Once'; - const always = 'Always Allow'; const deny = 'Deny'; + const always = 'Always Allow'; const choice = await vscode.window.showWarningMessage( summarizePermissionRequest(params), { modal: true }, + deny, allow, always, - deny, ); if (choice === allow && allowOptionId) { From 472f2266e24168731788348a0fb58f78be3df904 Mon Sep 17 00:00:00 2001 From: gitricko Date: Mon, 25 May 2026 00:13:48 -0400 Subject: [PATCH 04/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/extension.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 356f1f6..a4c5e32 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,9 +16,10 @@ function readApprovalMode(): boolean { try { const configPath = path.join(os.homedir(), '.hermes', 'config.yaml'); const content = fs.readFileSync(configPath, 'utf8'); - const match = /approvals:[\s\S]*?mode:\s*(\w+)/.exec(content); + const match = /(?:^|\n)approvals:\s*\n(?:[ \t]+.*\n)*?[ \t]+mode:\s*([^\s#]+)/m.exec(content); if (match) { - return match[1].trim() === 'true'; + const mode = match[1].trim().toLowerCase(); + return !['off', 'false', 'disabled', 'none', 'auto', 'yolo'].includes(mode); } } catch { } return true; // Default to safe mode if config missing/unparseable From 5fdc3c4990dd632b01487afb5572168fd16366c2 Mon Sep 17 00:00:00 2001 From: gitricko Date: Mon, 25 May 2026 00:18:44 -0400 Subject: [PATCH 05/12] Potential fix for pull request finding 'CodeQL / Inefficient regular expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/extension.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index a4c5e32..9f963df 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,10 +16,26 @@ function readApprovalMode(): boolean { try { const configPath = path.join(os.homedir(), '.hermes', 'config.yaml'); const content = fs.readFileSync(configPath, 'utf8'); - const match = /(?:^|\n)approvals:\s*\n(?:[ \t]+.*\n)*?[ \t]+mode:\s*([^\s#]+)/m.exec(content); - if (match) { - const mode = match[1].trim().toLowerCase(); - return !['off', 'false', 'disabled', 'none', 'auto', 'yolo'].includes(mode); + 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 mode = modeMatch[1].trim().toLowerCase(); + return !['off', 'false', 'disabled', 'none', 'auto', 'yolo'].includes(mode); + } + } } } catch { } return true; // Default to safe mode if config missing/unparseable From 32cc8bc49972e67c0164b9bfd96b49a251d33002 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 04:20:01 +0000 Subject: [PATCH 06/12] Fix security issue: prevent whitelisting of malformed permission requests The 'Always Allow' button now checks if the tool identifier is the fallback 'an action' (indicating a malformed permission request). If it is, the action is approved for this request only, but not persisted to the whitelist. This prevents a situation where a user could accidentally whitelist all future malformed/unknown permission requests by clicking 'Always Allow' once. Addresses review comment on PR #16 at discussion_r3296018150 --- src/extension.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 9f963df..a963582 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -307,8 +307,12 @@ export async function activate(context: vscode.ExtensionContext): Promise } if (choice === always && allowOptionId) { - outputChannel.appendLine(`[security] tool whitelisted: ${toolName}`); - await context.workspaceState.update(WHITELISTED_TOOLS_KEY, [...new Set([...whitelisted, toolName])]); + 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 }; } From bd80d91fea7d508f705dac813470898d48d171b9 Mon Sep 17 00:00:00 2001 From: gitricko Date: Mon, 25 May 2026 00:30:41 -0400 Subject: [PATCH 07/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index a963582..516a65f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -280,7 +280,7 @@ export async function activate(context: vscode.ExtensionContext): Promise : 'an action'; })(); const whitelisted = context.workspaceState.get(WHITELISTED_TOOLS_KEY, []); - + if (!approvalsRequired || whitelisted.includes(toolName)) { const allowOptionId = optionIdByIntent(params, 'allow'); if (allowOptionId) { From 1b0cd620ca170d641886bad5417ff13718fd64b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 04:32:09 +0000 Subject: [PATCH 08/12] Only show Allow buttons when optionIdByIntent returns a valid allowOptionId --- src/extension.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 516a65f..01cfc2a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -293,12 +293,11 @@ export async function activate(context: vscode.ExtensionContext): Promise const allow = 'Allow Once'; const deny = 'Deny'; const always = 'Always Allow'; + const buttons = allowOptionId ? [deny, allow, always] : [deny]; const choice = await vscode.window.showWarningMessage( summarizePermissionRequest(params), { modal: true }, - deny, - allow, - always, + ...buttons, ); if (choice === allow && allowOptionId) { From bbe114af5e2407330049a4db2b1888bb7f524a6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 04:36:11 +0000 Subject: [PATCH 09/12] Fix: Only present permission dialog buttons with valid optionIds --- src/extension.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 01cfc2a..eabf4a4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -293,7 +293,13 @@ export async function activate(context: vscode.ExtensionContext): Promise const allow = 'Allow Once'; const deny = 'Deny'; const always = 'Always Allow'; - const buttons = allowOptionId ? [deny, allow, always] : [deny]; + const buttons: string[] = []; + if (denyOptionId) { + buttons.push(deny); + } + if (allowOptionId) { + buttons.push(allow, always); + } const choice = await vscode.window.showWarningMessage( summarizePermissionRequest(params), { modal: true }, From cf83fcc9385107d3a48fe6b311ded3f9024b5c0a Mon Sep 17 00:00:00 2001 From: gitricko Date: Mon, 25 May 2026 00:42:44 -0400 Subject: [PATCH 10/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/extension.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index eabf4a4..a394391 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -32,7 +32,8 @@ function readApprovalMode(): boolean { const modeMatch = /^\s*mode:\s*([^\s#]+)/.exec(line); if (modeMatch) { - const mode = modeMatch[1].trim().toLowerCase(); + const rawMode = modeMatch[1].trim().toLowerCase(); + const mode = rawMode.replace(/^['"]|['"]$/g, ''); return !['off', 'false', 'disabled', 'none', 'auto', 'yolo'].includes(mode); } } From 8c38de06559b23a556b59f8c947b228efbecb3e5 Mon Sep 17 00:00:00 2001 From: gitricko Date: Mon, 25 May 2026 00:52:42 -0400 Subject: [PATCH 11/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/extension.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index a394391..ed987ac 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -272,16 +272,17 @@ export async function activate(context: vscode.ExtensionContext): Promise const toolName = (() => { if (!params || typeof params !== 'object') return 'an action'; const record = params as Record; - return typeof record.toolName === 'string' + 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) { From 6e5b3400e3a8464031c225960eae2cf83755a569 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 04:57:59 +0000 Subject: [PATCH 12/12] Sanitize permission prompt message by removing CR/LF characters --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index ed987ac..9acbda9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -303,7 +303,7 @@ export async function activate(context: vscode.ExtensionContext): Promise buttons.push(allow, always); } const choice = await vscode.window.showWarningMessage( - summarizePermissionRequest(params), + summarizePermissionRequest(params).replace(/[\r\n]+/g, ' '), { modal: true }, ...buttons, );