Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Comment thread
gitricko marked this conversation as resolved.
"publisher": "gitricko",
"author": "gitricko",
"license": "MIT",
Expand All @@ -17,7 +17,7 @@
"llm",
"tool use",
"chat",
"hermes",
"hermes agent",
"acp",
"agent client protocol",
"sidebar chat",
Expand Down Expand Up @@ -67,7 +67,7 @@
"activitybar": [
{
"id": "hermes",
"title": "Hermes Agent",
"title": "Hermes Code Agent",
"icon": "resources/hermes-icon.svg"
}
]
Expand Down
77 changes: 74 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Comment thread
gitricko marked this conversation as resolved.
}
}
} catch { }
return true; // Default to safe mode if config missing/unparseable
}
Comment thread
gitricko marked this conversation as resolved.
function extractModelFromHermesConfig(content: string): string | null {
const lines = content.split(/\r?\n/);

Expand Down Expand Up @@ -235,23 +266,63 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
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<string, unknown>;
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<string[]>(WHITELISTED_TOOLS_KEY, []);
if (!approvalsRequired || whitelisted.includes(toolName)) {
Comment thread
gitricko marked this conversation as resolved.
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';
Comment on lines 293 to +297
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,
);
Comment on lines 293 to 309
Comment on lines 305 to 309

if (choice === allow && allowOptionId) {
outputChannel.appendLine('[security] permission granted once');
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 };
Expand Down
Loading