--- name: harnessed-agent-skill-architecture description: Understanding the triple-layer skill management architecture in Hermes Agent systems - shared file skills, per-user file skills, and database-backed skills author: Hermes Agent tags: [hermes-agent, skills, architecture, multi-user, database, isolation] --- # Hermes Agent Triple-Layer Skill Architecture ## Overview Hermes Agent implements **three skill layers** that serve different purposes with different isolation models: 1. **Shared File Skills** (`~/.hermes/skills/`) - Read by all, writable only by owner organization (org_id='0') 2. **Per-User File Skills** (`~/.hermes/users/{user_id}/skills/`) - Created during AI interactions, isolated per user 3. **Database Skills** (`hermes_skills` table) - 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.md` and 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 ```python 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_manage` in `harnessed_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_manage` tool, 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 1. User asks AI to "remember this as a skill" during a conversation 2. Reasoning engine generates a plan with `skill_manage` tool call 3. `_execute_tool` passes `context={user_id: X}` to `harnessed_execute_tool` 4. Tool wrapper receives context, resolves user directory via `_get_user_dir()` 5. Skill is written to `~/.hermes/users/{user_id}/skills/{name}/SKILL.md` ### User Dir Resolution Pattern ```python 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_id` field ### 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` (NOT `harnessed_skills`) — common bug ## Skill Resolution Order ### skill_view (read) 1. Check `~/.hermes/users/{user_id}/skills/{name}/SKILL.md` (user skill) 2. Check `~/.hermes/skills/{name}/SKILL.md` (shared skill) 3. Return error if not found ### skills_list (list) 1. List all `~/.hermes/users/{user_id}/skills/` (source='user') 2. List all `~/.hermes/skills/` (source='shared'), skip duplicates by name 3. 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`: ```python # 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 isolation - `wrapped_memory` — per-user `memory.json` - `wrapped_todo` — per-user `todo.json` - `wrapped_execute_code` — per-user `tmp/` 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: ```python # 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: 1. DB skills (`hermes_skills` table) via sor.R with keyword LIKE 2. User file skills (`~/.hermes/users/{user_id}/skills/`) via directory scan 3. 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 |