9.3 KiB
| name | description | author | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|
| harnessed-agent-skill-architecture | Understanding the triple-layer skill management architecture in Hermes Agent systems - shared file skills, per-user file skills, and database-backed skills | Hermes Agent |
|
Hermes Agent Triple-Layer Skill Architecture
Overview
Hermes Agent implements three skill layers that serve different purposes with different isolation models:
- Shared File Skills (
~/.hermes/skills/) - Read by all, writable only by owner organization (org_id='0') - Per-User File Skills (
~/.hermes/users/{user_id}/skills/) - Created during AI interactions, isolated per user - Database Skills (
hermes_skillstable) - DB-backed, managed by harnessed_agent core.py
Understanding this distinction is crucial for proper system design and troubleshooting.
Directory Structure
~/.hermes/
├── skills/ # SHARED: all users read, owner org writes
│ ├── module-development-spec/
│ │ └── SKILL.md
│ └── bricks-framework/
│ └── SKILL.md
│
└── users/
├── {user_id_1}/
│ ├── skills/ # PRIVATE: user 1 only
│ │ ├── my-custom-skill/
│ │ │ └── SKILL.md
│ ├── memory.json
│ ├── todo.json
│ └── tmp/
└── {user_id_2}/
├── skills/ # PRIVATE: user 2 only
└── ...
Layer 1: Shared File Skills
Location and Structure
- Root Directory:
~/.hermes/skills/ - Format: Skill name subdirectories containing
SKILL.mdand optional support files - Example Path:
~/.hermes/skills/module-development-spec/SKILL.md
Characteristics
- Readable by ALL users: Available to every user regardless of org
- Writable ONLY by owner org: Users with
org_id='0'can create/modify/delete - File-based Storage: Skills stored as actual files on disk
- Installation: Pre-installed during Hermes Agent initial setup
Owner Org Check Pattern
def _is_owner_org(context=None):
# 1. Check context for org_id
if context:
org_id = context.get('org_id') or context.get('orgid')
if org_id is not None:
return str(org_id) == '0'
# 2. Fall back to ServerEnv
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', None)
return str(org_id) == '0' if org_id else False
Management
- Managed by
wrapped_skill_manageinharnessed_agent/tools/base_tools.py - Operations require
source='shared'kwarg AND owner org membership - Non-owner users can VIEW and LIST shared skills but cannot CREATE/PATCH/EDIT/DELETE
Layer 2: Per-User File Skills
Location and Structure
- Root Directory:
~/.hermes/users/{user_id}/skills/ - Format: Skill name subdirectories containing
SKILL.md - Example Path:
~/.hermes/users/usr123/skills/my-workflow/SKILL.md
Characteristics
- Created by AI tool execution: When reasoning engine calls
skill_managetool, it creates skills here - User-isolated: Each user has their own directory; no cross-user visibility
- Full CRUD: Owner can create, read, update, delete without restriction
- Coexists with user memory: Same directory contains
memory.json,todo.json,tmp/
How They're Created
- User asks AI to "remember this as a skill" during a conversation
- Reasoning engine generates a plan with
skill_managetool call _execute_toolpassescontext={user_id: X}toharnessed_execute_tool- Tool wrapper receives context, resolves user directory via
_get_user_dir() - Skill is written to
~/.hermes/users/{user_id}/skills/{name}/SKILL.md
User Dir Resolution Pattern
def _get_user_dir(base_dir, context=None):
user_id = context.get('user_id') if context else None
if user_id:
return os.path.join(base_dir, "users", str(user_id))
return base_dir # fallback to global dir
Layer 3: Database Skills
Location and Structure
- Storage: Database table
hermes_skills - Schema:
id,user_id,name,description,content,category,version,is_active,created_at,updated_at - Isolation: Skills filtered by
user_idfield
Characteristics
- Managed by harnessed_agent core.py:
manage_skills()method - API-level: Accessed via
harnessed_manage_skills()module function - User-isolated: Each query filtered by
user_id - CRITICAL: Table name is
hermes_skills(NOTharnessed_skills) — common bug
Skill Resolution Order
skill_view (read)
- Check
~/.hermes/users/{user_id}/skills/{name}/SKILL.md(user skill) - Check
~/.hermes/skills/{name}/SKILL.md(shared skill) - Return error if not found
skills_list (list)
- List all
~/.hermes/users/{user_id}/skills/(source='user') - List all
~/.hermes/skills/(source='shared'), skip duplicates by name - Return combined list with source indicator
skill_manage (write)
- Default
source='user'→ writes to~/.hermes/users/{user_id}/skills/ - With
source='shared'→ checks owner org, then writes to~/.hermes/skills/
Tool Wrapper Context Injection Pattern
Tool wrappers in base_tools.py need context parameter for user isolation. The injection is automatic via core.py:
# In HermesAgent._execute_tool_with_retry():
import inspect
sig = inspect.signature(tool_func)
if 'context' in sig.parameters:
params_with_context['context'] = context
Tool wrappers that accept context:
wrapped_skill_view,wrapped_skills_list,wrapped_skill_manage— skills directory isolationwrapped_memory— per-usermemory.jsonwrapped_todo— per-usertodo.jsonwrapped_execute_code— per-usertmp/directory
Reasoning Engine Integration
Context Propagation Chain
User request -> reasoning_console.wss -> reason_and_execute(user_id=X)
-> self._current_user_id = X, self._current_org_id = orgid from ServerEnv
-> context = {"user_id": X, "org_id": self._current_org_id}
-> _execute_tool(tool, params, context)
-> harnessed_execute_tool(tool, params, context) # passes context
-> agent.execute_tool_call(tool, params, context) # passes context
-> _execute_tool_with_retry(func, params, context) # injects context
-> wrapped_skill_manage(..., context) # receives context
WebSocket Push Isolation
The reasoning engine uses ws_push_callbacks: Dict[str, callable] (keyed by user_id) instead of a single shared ws_push callback. This prevents cross-user event leakage:
# In reasoning_console.wss:
engine.ws_push_callbacks[user_id] = lambda msg: _ws_push(user_id, msg)
# In core.py _push():
if user_id and user_id in self.ws_push_callbacks:
await self.ws_push_callbacks[user_id](msg)
Skill Discovery in Reasoning
_find_relevant_skills() searches:
- DB skills (
hermes_skillstable) via sor.R with keyword LIKE - User file skills (
~/.hermes/users/{user_id}/skills/) via directory scan - Shared skills (
~/.hermes/skills/) via directory scan Results are deduplicated by name, limited to 5.
Permission Matrix
| Operation | Shared Skills | User Skills | DB Skills |
|---|---|---|---|
| List | All users | Owner only | Owner only (user_id filter) |
| Read | All users | Owner only | Owner only (user_id filter) |
| Create | Owner org only | Owner only | Owner only (user_id filter) |
| Update | Owner org only | Owner only | Owner only (user_id filter) |
| Delete | Owner org only | Owner only | Owner only (user_id filter) |
Common Pitfalls
❌ Wrong table name: harnessed_skills vs hermes_skills
Reality: The model definition uses hermes_skills. Code referencing harnessed_skills will query a non-existent table.
❌ Assuming shared skills are writable by all
Reality: Shared skills (~/.hermes/skills/) are read-only for non-owner-org users. Write operations return "共享技能仅允许所有者机构用户修改".
❌ Tool wrappers not receiving user context
Reality: _execute_tool in reasoning/core.py MUST pass context to harnessed_execute_tool. Without it, tools fall back to global directories.
❌ Using a single ws_push callback for all users
Reality: The reasoning engine's WebSocket push must be per-user via ws_push_callbacks dict keyed by user_id, otherwise events from one user leak to another.
Key Differences Summary
See references/context-injection-pattern.md for the technical details of how context flows through the tool execution chain.
| Aspect | Shared File Skills | Per-User File Skills | Database Skills |
|---|---|---|---|
| Storage | File system (~/.hermes/skills/) |
File system (~/.hermes/users/{uid}/skills/) |
Database (hermes_skills table) |
| Read Access | All users | Owner only | Owner only |
| Write Access | Owner org (org_id='0') only | Owner only | Owner only |
| Creation | Setup/pre-installed or owner org | Tool execution during AI sessions | API calls via core.py |
| Use Case | System-wide knowledge base | User-created skills from conversations | Programmatic skill management |