| name | hermes-control-interface-dashboard |
| description | Self-hosted web dashboard for managing Hermes AI agent stacks with terminals, file explorer, multi-agent gateway, and RBAC |
| triggers | ["set up hermes control interface dashboard","manage hermes agents through web ui","configure hermes control interface","deploy hermes dashboard with rbac","use hermes control interface api","troubleshoot hermes control interface","create users in hermes control","manage hermes gateway profiles"] |
Hermes Control Interface Dashboard
Skill by ara.so — Hermes Skills collection.
A self-hosted web dashboard for managing the Hermes AI agent stack. Provides browser-based terminal, file explorer, session management, cron jobs, multi-agent gateway control, token analytics, and RBAC — all behind password authentication.
Stack: Vanilla JS + Vite, Node.js, Express, WebSocket, xterm.js
Port: 10272 (default)
Version: 3.5.0
Installation
Prerequisites
sudo apt-get install -y python3 make g++
brew install python3
Manual Installation (Recommended)
git clone https://github.com/xaspx/hermes-control-interface.git
cd hermes-control-interface
npm install
cp .env.example .env
openssl rand -hex 32
nano .env
Environment Configuration
Minimal .env:
HERMES_CONTROL_PASSWORD=your-secure-password-here
HERMES_CONTROL_SECRET=<output-from-openssl-rand-hex-32>
PORT=10272
NODE_ENV=production
HERMES_CONTROL_ROOTS=~/.hermes,~/Documents
Build and Run
npm run build
npm start
npm run dev
Production Deployment (Systemd)
sudo tee /etc/systemd/system/hermes-control.service > /dev/null <<EOF
[Unit]
Description=Hermes Control Interface
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$(pwd)
ExecStart=/usr/bin/node server.js
Restart=always
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable hermes-control
sudo systemctl start hermes-control
sudo systemctl status hermes-control
Core Features
Authentication & RBAC
HCI supports multi-user authentication with role-based access control:
Roles:
admin — Full access (all 20 permissions)
viewer — Read-only access
custom — Granular permission selection
Key Permissions:
agents:read, agents:write, agents:delete
chat:read, chat:write
sessions:read, sessions:write, sessions:delete
config:read, config:write
gateway:control, gateway:logs
files:read, files:write
terminal:exec
users:manage
system:maintain
cron:manage
Creating Users (via Maintenance → Users):
{
"username": "alice",
"password": "secure-password",
"role": "viewer"
}
Multi-Agent Gateway Management
Profile Structure:
~/.hermes/
├── profiles/
│ ├── default/
│ │ └── config.yaml
│ ├── production/
│ │ └── config.yaml
│ └── testing/
│ └── config.yaml
API: List Profiles
fetch('/api/agents/profiles', {
headers: {
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
}
})
.then(res => res.json())
.then(data => {
});
API: Start/Stop Gateway
fetch('/api/agents/gateway/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ profile: 'production' })
})
.then(res => res.json())
.then(data => console.log(data.message));
API: Create Profile
fetch('/api/agents/profiles', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
name: 'staging',
cloneFrom: 'default'
})
});
Chat Interface
Session Management:
const sendMessage = async (message, sessionId = null) => {
const args = sessionId
? ['--continue', sessionId, message]
: ['--continue', '', message];
const response = await fetch('/api/chat/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
profile: 'default',
args: args,
suppressBanner: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.();
( line lines) {
(line.()) {
data = .(line.());
(data. === ) {
.(, data.);
} (data. === ) {
.(, data., , data.);
} (data. === ) {
.(, data.);
}
}
}
}
};
();
(, );
List Sessions:
fetch('/api/chat/sessions?profile=default', {
headers: { 'Authorization': `Bearer ${sessionToken}` }
})
.then(res => res.json())
.then(data => {
});
Rename Session:
fetch('/api/chat/sessions/abc123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ title: 'Weather Research' })
});
Token Analytics
Get Usage Stats:
fetch('/api/usage/stats?range=7d&profile=default', {
headers: { 'Authorization': `Bearer ${sessionToken}` }
})
.then(res => res.json())
.then(data => {
console.log('Sessions:', data.totalSessions);
console.log('Tokens:', data.totalTokens);
console.log('Cost:', data.estimatedCost);
console.log('Models:', data.modelBreakdown);
console.log('Platforms:', data.platformBreakdown);
console.log('Top Tools:', data.topTools);
});
Query Ranges: today, 7d, 30d, 90d
Configuration Management
Get Config:
fetch('/api/agents/config?profile=default', {
headers: { 'Authorization': `Bearer ${sessionToken}` }
})
.then(res => res.json())
.then(data => {
console.log('Config YAML:', data.config);
console.log('Categories:', data.categories);
});
Update Config:
fetch('/api/agents/config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
profile: 'default',
config: `
llm:
model: hermes-3
provider: openrouter
temperature: 0.7
platforms:
telegram:
enabled: true
token: \${TELEGRAM_BOT_TOKEN}
memory:
provider: honcho
honcho_url: http://localhost:8000
`
})
});
Reset Category to Defaults:
fetch('/api/agents/config/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
profile: 'default',
category: 'llm'
})
});
Cron Job Management
List Cron Jobs:
fetch('/api/cron?profile=default', {
headers: { 'Authorization': `Bearer ${sessionToken}` }
})
.then(res => res.json())
.then(data => {
});
Create Cron Job:
fetch('/api/cron', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
profile: 'default',
schedule: '0 */6 * * *',
command: 'hermes chat "Check system health"',
enabled: true
})
});
Run Job Immediately:
fetch('/api/cron/job1/run', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ profile: 'default' })
});
File Explorer
List Directory:
fetch('/api/files?path=~/.hermes', {
headers: { 'Authorization': `Bearer ${sessionToken}` }
})
.then(res => res.json())
.then(data => {
console.log('Files:', data.files);
});
Read File:
fetch('/api/files/read?path=~/.hermes/config.yaml', {
headers: { 'Authorization': `Bearer ${sessionToken}` }
})
.then(res => res.json())
.then(data => {
console.log('Content:', data.content);
});
Write File:
fetch('/api/files/write', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
path: '~/.hermes/custom-prompt.txt',
content: 'You are a helpful assistant specializing in DevOps.'
})
});
Security Note: All paths are validated and scoped to HERMES_CONTROL_ROOTS (default: ~/.hermes). Path traversal attacks are prevented.
Terminal (WebSocket)
Connect to Terminal:
const ws = new WebSocket(`ws://${location.host}/terminal`);
ws.onopen = () => {
console.log('Terminal connected');
ws.send(JSON.stringify({
type: 'auth',
token: sessionToken
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'output') {
console.log('Output:', data.data);
} else if (data.type === 'error') {
console.error('Error:', data.message);
}
};
ws.send(JSON.stringify({
type: 'input',
data: 'hermes doctor\n'
}));
ws.send(JSON.stringify({
type: 'resize',
: ,
:
}));
Rate Limiting: 30 commands per minute per IP.
System Maintenance
Run Doctor (Diagnostics):
fetch('/api/maintenance/doctor', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
}
})
.then(res => res.json())
.then(data => {
console.log('Issues found:', data.issues);
console.log('Fixes applied:', data.fixes);
});
Generate Debug Dump:
fetch('/api/maintenance/dump', {
headers: { 'Authorization': `Bearer ${sessionToken}` }
})
.then(res => res.json())
.then(data => {
console.log('System info:', data.system);
console.log('Config:', data.config);
console.log('Logs:', data.logs);
});
Backup System:
window.location.href = '/api/maintenance/backup?token=' + sessionToken;
Restart HCI Server:
fetch('/api/maintenance/restart-hci', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
}
});
Common Patterns
Programmatic Profile Switching
class HermesProfileManager {
constructor(baseUrl, token, csrfToken) {
this.baseUrl = baseUrl;
this.token = token;
this.csrfToken = csrfToken;
}
async switchProfile(from, to) {
await fetch(`${this.baseUrl}/api/agents/gateway/stop`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`,
'X-CSRF-Token': this.csrfToken
},
body: JSON.stringify({ profile: from })
});
await fetch(`${this.baseUrl}/api/agents/gateway/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': ,
: .
},
: .({ : to })
});
.();
}
() {
(, {
: ,
: {
: ,
: ,
: .
},
: .({ name, : baseProfile })
});
config = (, {
: { : }
}).( res.());
modifiedConfig = config..(, );
(, {
: ,
: {
: ,
: ,
: .
},
: .({ : name, : modifiedConfig })
});
.();
}
}
manager = (, sessionToken, csrfToken);
manager.();
manager.(, );
Bulk Session Export
async function exportAllSessions(profile) {
const sessions = await fetch(`/api/chat/sessions?profile=${profile}`, {
headers: { 'Authorization': `Bearer ${sessionToken}` }
}).then(res => res.json());
const exports = [];
for (const session of sessions.sessions) {
const data = await fetch(`/api/chat/sessions/${session.id}/export?profile=${profile}`, {
headers: { 'Authorization': `Bearer ${sessionToken}` }
}).then(res => res.json());
exports.push({
id: session.id,
title: session.title,
messages: data.messages
});
}
const blob = new Blob([JSON.stringify(exports, , )], { : });
url = .(blob);
a = .();
a. = url;
a. = ;
a.();
}
();
Custom Token Usage Reporter
class TokenUsageReporter {
constructor(baseUrl, token) {
this.baseUrl = baseUrl;
this.token = token;
}
async generateReport(profile, range = '30d') {
const stats = await fetch(
`${this.baseUrl}/api/usage/stats?range=${range}&profile=${profile}`,
{ headers: { 'Authorization': `Bearer ${this.token}` } }
).then(res => res.json());
const report = {
period: range,
profile: profile,
summary: {
totalSessions: stats.totalSessions,
totalMessages: stats.totalMessages,
totalTokens: stats.totalTokens,
estimatedCost: stats.estimatedCost,
avgTokensPerSession: Math.round(stats.totalTokens / stats.totalSessions)
},
topModels: stats.modelBreakdown.slice(, ),
: stats..(, ),
: stats.
};
.(report.);
.(report.);
report;
}
() {
[statsA, statsB] = .([
(,
{ : { : } }).( res.()),
(,
{ : { : } }).( res.())
]);
{
: { : profileA, : statsA., : statsA. },
: { : profileB, : statsB., : statsB. },
: statsA. - statsB.,
: statsA. - statsB.
};
}
}
reporter = (, sessionToken);
report = reporter.(, );
.(, report..);
comparison = reporter.(, , );
.(, comparison.);
Configuration Examples
Multi-Platform Setup
~/.hermes/profiles/production/config.yaml:
llm:
model: hermes-3
provider: openrouter
api_key: ${OPENROUTER_API_KEY}
temperature: 0.7
max_tokens: 4000
platforms:
telegram:
enabled: true
token: ${TELEGRAM_BOT_TOKEN}
whatsapp:
enabled: true
account_sid: ${TWILIO_ACCOUNT_SID}
auth_token: ${TWILIO_AUTH_TOKEN}
from_number: ${TWILIO_PHONE_NUMBER}
slack:
enabled: true
bot_token: ${SLACK_BOT_TOKEN}
app_token: ${SLACK_APP_TOKEN}
memory:
provider: honcho
honcho_url: http://localhost:8000
honcho_app_name: hermes-prod
tools:
web_search:
enabled: true
provider: tavily
api_key: ${TAVILY_API_KEY}
code_execution:
enabled: true
sandbox: docker
timeout:
Custom RBAC Setup
Create a "Analyst" role (read-only + chat):
const analystPermissions = [
'agents:read',
'chat:read',
'chat:write',
'sessions:read',
'config:read',
'gateway:logs'
];
await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${adminToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
username: 'analyst1',
password: 'secure-password',
role: 'custom',
permissions: analystPermissions
})
});
Scheduled Maintenance Cron
await fetch('/api/cron', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
profile: 'default',
schedule: '0 3 * * *',
command: 'hermes doctor --auto-fix && hermes chat "Daily system report"',
enabled: true
})
});
await fetch('/api/cron', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
profile: 'default',
schedule: '0 2 * * 0',
command: 'cd ~/.hermes && tar -czf backup-$(date +%Y%m%d).tar.gz .',
enabled: true
})
});
Troubleshooting
Gateway Won't Start
Symptom: "Failed to start gateway" error
Solutions:
sudo lsof -i :8000
journalctl -u hermes-gateway-default -n 50
systemctl status hermes-gateway-default
hermes gateway start --profile default --debug
hermes config validate --profile default
WebSocket Connection Fails
Symptom: Terminal or chat streaming doesn't work
Solutions:
Session Not Resuming
Symptom: --continue <session-id> creates new session instead
Solutions:
ls -la ~/.hermes/sessions/
hermes chat --continue abc123 "test message"
chmod 644 ~/.hermes/sessions/abc123.json
Token Analytics Not Showing
Symptom: Usage page shows zero data
Solutions:
ls -la ~/.hermes/sessions/*.json
cat ~/.hermes/profiles/default/config.yaml | grep -A5 analytics
hermes analytics rebuild
tail -f ~/.hermes/hci.log | grep analytics
Permission Denied Errors
Symptom: "Permission denied" when accessing files/terminals
Solutions:
ls -ld ~/.hermes
chmod 755 ~/.hermes
echo $SHELL
High Memory Usage
Symptom: HCI process consuming excessive RAM
Solutions:
systemctl restart hermes-control
node --expose-gc server.js
CSRF Token Mismatch
Symptom: "Invalid CSRF token" on form submissions
Solutions:
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/agents/profiles', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'test' })
});
document.cookie.split(";").forEach(c => {
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
});
Gateway Logs Not Streaming
Symptom: Gateway log viewer shows "Connecting..." indefinitely
Solutions:
systemctl status hermes-gateway-default
sudo journalctl -u hermes-gateway-default --no-pager -n 10
sudo user