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
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

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

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:

# 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:

# 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