← openharness report

T16 — Plugin manifest-based loading

OpenHarness plugin system đóng gói skills, hooks, MCP configs, và agents vào một distributable unit — được discover tự động qua plugin.json manifest và command namespace để tránh conflict.
Nhóm: D — Extension EcosystemFile: plugins/loader.py:104-157Search dirs: ~/.openharness/plugins/ · .openharness/plugins/ID: D.3

Tổng quan Overview

Plugin là đơn vị phân phối lớn nhất trong OpenHarness extension ecosystem — cao hơn skill (T14) và hook (T15). Một plugin bundle tất cả extension types cần thiết cho một use-case hoàn chỉnh vào một thư mục duy nhất với plugin.json manifest.

Punchline từ Claude Code Extensions guide: "Plugins are installable bundles combining skills, agents, hooks, and MCP configs into a single distributable unit." Điều này cho phép distribute một "security audit toolkit" hay "deployment assistant" như một package duy nhất — không phải hướng dẫn user copy 4 loại file vào 4 chỗ khác nhau.
Command namespace: "The command namespace plugin:plugin-name:command prevents skill name collision between plugins." Nếu plugin A và plugin B đều có skill tên test, chúng sống ở plugin:pluginA:testplugin:pluginB:test — không bao giờ conflict. Đây là design choice quan trọng cho plugin ecosystem lớn.
"A plugin without a manifest file is ignored silently — validate your plugin.json schema first." Đây là silent failure pattern — không có warning, plugin đơn giản không tồn tại trong system. Luôn validate manifest trước khi distribute plugin.

Phân tích code: plugins/loader.py Anatomy

Plugin dataclass — manifest fields

Plugin dataclass map trực tiếp từ plugin.json fields, với tất cả path fields là optional (Path | None) — plugin có thể chỉ có skills mà không có hooks, hoặc chỉ có MCP config.

plugins/loader.py — Plugin dataclass + load_plugins()

TS
{`
@dataclass
class Plugin:
    name: str
    version: str
    description: str
    base_dir: Path
    skills_dir: Path | None
    hooks_file: Path | None
    mcp_file: Path | None
    agents_dir: Path | None

def load_plugins(search_dirs: list[Path]) -> list[Plugin]:
    plugins = []
    for search_dir in search_dirs:
        if not search_dir.exists():
            continue
        for plugin_dir in sorted(search_dir.iterdir()):
            manifest = plugin_dir / "plugin.json"
            if not manifest.exists():
                continue  # silently ignored
            try:
                data = json.loads(manifest.read_text())
                plugin = Plugin(
                    name=data["name"],
                    version=data.get("version", "0.0.0"),
                    description=data.get("description", ""),
                    base_dir=plugin_dir,
                    skills_dir=_resolve(plugin_dir, data.get("skills_dir")),
                    hooks_file=_resolve(plugin_dir, data.get("hooks_file")),
                    mcp_file=_resolve(plugin_dir, data.get("mcp_file")),
                    agents_dir=_resolve(plugin_dir, data.get("agents_dir")),
                )
                plugins.append(plugin)
            except (KeyError, json.JSONDecodeError) as e:
                logger.warning(f"Plugin {plugin_dir.name}: invalid manifest: {e}")
    return plugins
`}

sorted(search_dir.iterdir()) đảm bảo load order deterministic — alphabetical sort theo plugin directory name. Khi có conflict (2 plugins cùng skill name), plugin load trước (theo alphabet) win. Đây là first-wins by sort order policy.

plugin.json manifest — ví dụ thực tế

~/.openharness/plugins/security-toolkit/plugin.json

JSON
{`
{
  "name": "security-toolkit",
  "version": "1.2.0",
  "description": "Security auditing skills + SAST hooks",
  "skills_dir": "skills/",
  "hooks_file": "hooks.yaml",
  "mcp_file": "mcp.json",
  "agents_dir": "agents/"
}
`}

Tất cả path fields là relative so với base_dir của plugin. _resolve(plugin_dir, relative_path) join chúng thành absolute path. Nếu field bị omit (ví dụ không có agents_dir), value là None và downstream code bỏ qua gracefully.

Search dirs — global vs project-local

Plugin search order: 1. ~/.openharness/plugins/ ← Global user plugins security-toolkit/ plugin.json skills/ hooks.yaml 2. .openharness/plugins/ ← Project-local plugins (CWD) company-deploy/ plugin.json skills/ hooks.yaml Project-local plugins load SAU global → có thể override global plugins (nếu cùng tên, first-wins theo sort, nhưng project-local search_dir được pass vào search_dirs list với priority theo caller)

Tương tác với các kỹ thuật khác Interaction

T16 (Plugin manifest) tương tác với: T14 (Skill loader) └─ Plugin.skills_dir → pass vào load_all_skills(plugin_dirs=[...]) Skills từ plugin được namespaced: plugin:security-toolkit:audit Cùng 3-stage loading như bundled skills T15 (Hook system) └─ Plugin.hooks_file → path của hooks.yaml do plugin cung cấp HookExecutor nhận hook list từ plugin hooks file Hooks từ plugin merge với user hooks (dedup theo event+command) T17 (MCP integration) └─ Plugin.mcp_file → MCP server config bundle cùng plugin Ví dụ: security-toolkit bundled với semgrep MCP server User không phải configure MCP riêng — plugin lo hết Agent system └─ Plugin.agents_dir → sub-agents contribute bởi plugin Agents có thể được invoke từ skill hoặc hook trong cùng plugin
Plugin là orchestration layer — không implement logic trực tiếp mà wire các extension types lại với nhau. Một plugin "security-toolkit" có thể: (1) đăng ký skill audit-code, (2) đăng ký PRE_TOOL_USE hook check dangerous commands, (3) configure MCP server Semgrep — tất cả tự động khi user install plugin.

Failure modes Failures

Failure 1: Missing 'name' → KeyError, plugin bị bỏ qua

Manifest thiếu required field 'name'

JSON
{`
# plugin.json thiếu "name" field:
{
  "version": "1.0.0",
  "description": "My plugin",
  "skills_dir": "skills/"
  // "name" bị quên → KeyError khi data["name"]
}

# Kết quả: logger.warning("Plugin my-plugin: invalid manifest: 'name'")
# Plugin không được load SILENT fail từ user perspective
# User install plugin, invoke /plugin:my-plugin:skill "not found"

# Fix: validate manifest trước khi distribute
import jsonschema
MANIFEST_SCHEMA = {
    "required": ["name"],
    "properties": {
        "name": {"type": "string", "pattern": "^[a-z0-9-]+$"},
        "version": {"type": "string"},
    }
}
jsonschema.validate(manifest_data, MANIFEST_SCHEMA)
`}

Failure 2: Path traversal — skills_dir trỏ ra ngoài plugin dir

Security concern: relative path resolution

JSON
{`
# Malicious hoặc buggy plugin.json:
{
  "name": "evil-plugin",
  "skills_dir": "../../.openharness/skills/"
  // → _resolve() join thành absolute path pointing OUTSIDE plugin dir
  // → Load skills từ user skills dir dưới namespace plugin:evil-plugin:*
  // → Có thể shadow user skills với crafted versions
}

# Mitigation: validate resolved path vẫn nằm trong plugin base_dir
def _resolve(base_dir: Path, relative: str | None) -> Path | None:
    if relative is None:
        return None
    resolved = (base_dir / relative).resolve()
    # Security check: ensure no path traversal
    if not str(resolved).startswith(str(base_dir.resolve())):
        raise ValueError(f"Path traversal detected: {relative}")
    return resolved
`}

Failure 3: Plugin name conflict — first-wins by sort order

Hai plugins định nghĩa cùng skill name

TS
{`
# ~/.openharness/plugins/
#   aaa-plugin/plugin.json  →  name: "toolkit", skills: [audit]
#   zzz-plugin/plugin.json  →  name: "toolkit", skills: [audit]  # CONFLICT

# sorted(search_dir.iterdir()) → aaa-plugin load trước
# "aaa-plugin/skills/audit" được load, seen_names = {"audit"}
# "zzz-plugin/skills/audit" bị skip vì "audit" đã trong seen_names

# User không biết zzz-plugin/audit bị shadowed
# Không có warning nào được emit

# Mitigation: dùng namespaced skill names trong plugin
# Thay vì "audit" → "security-audit" hoặc "toolkit-audit"
# Namespace command: plugin:aaa-plugin:audit (tự động không conflict)
`}

So sánh với các harness khác Compare

HarnessPlugin formatAuto-loadNamespaceComponents
OpenHarnessplugin.json manifestYes — scan ~/.openharness/plugins/ + project-localplugin:name:cmdskills + hooks + mcp + agents
Claude CodePlugin via CLI / marketplaceYes — namespaced auto-discoverNamespacedskills + hooks + mcp
AiderN/AN/AN/AN/A
LangGraphPython package (pip)No — explicit importPython module namespaceTools + nodes

OpenHarness và Claude Code có mô hình plugin gần nhau nhất — cả hai đều có manifest, auto-scan, và namespacing. Điểm khác biệt chính: OpenHarness thêm agents_dir trong manifest (Claude Code chưa có trong spec public), và OpenHarness scan cả project-local plugin dir (.openharness/plugins/) — hữu ích cho monorepo có plugin riêng per-project.

Implementation recipe Recipe

Minimal plugin loader với manifest validation và path safety check:

plugin_loader.py — minimal implementation

PY
{`
import json
from dataclasses import dataclass
from pathlib import Path
import logging

logger = logging.getLogger(__name__)

@dataclass
class Plugin:
    name: str
    version: str
    description: str
    base_dir: Path
    skills_dir: Path | None = None
    hooks_file: Path | None = None
    mcp_file: Path | None = None
    agents_dir: Path | None = None

def _resolve_safe(base: Path, rel: str | None) -> Path | None:
    if rel is None:
        return None
    resolved = (base / rel).resolve()
    if not str(resolved).startswith(str(base.resolve())):
        raise ValueError(f"Path traversal: {rel!r} escapes plugin dir")
    return resolved

def load_plugins(search_dirs: list[Path]) -> list[Plugin]:
    plugins: list[Plugin] = []
    for search_dir in search_dirs:
        if not search_dir.exists():
            continue
        for plugin_dir in sorted(search_dir.iterdir()):
            if not plugin_dir.is_dir():
                continue
            manifest = plugin_dir / "plugin.json"
            if not manifest.exists():
                continue  # silently skip — by design
            try:
                data = json.loads(manifest.read_text(encoding="utf-8"))
                plugins.append(Plugin(
                    name=data["name"],          # required — KeyError if missing
                    version=data.get("version", "0.0.0"),
                    description=data.get("description", ""),
                    base_dir=plugin_dir,
                    skills_dir=_resolve_safe(plugin_dir, data.get("skills_dir")),
                    hooks_file=_resolve_safe(plugin_dir, data.get("hooks_file")),
                    mcp_file=_resolve_safe(plugin_dir, data.get("mcp_file")),
                    agents_dir=_resolve_safe(plugin_dir, data.get("agents_dir")),
                ))
            except (KeyError, json.JSONDecodeError, ValueError) as e:
                logger.warning(f"Plugin {plugin_dir.name}: skipped — {e}")
    return plugins

# Usage:
GLOBAL_PLUGINS = Path.home() / ".openharness" / "plugins"
LOCAL_PLUGINS = Path.cwd() / ".openharness" / "plugins"
plugins = load_plugins([GLOBAL_PLUGINS, LOCAL_PLUGINS])
`}
Sau khi load plugins, pass [p.skills_dir for p in plugins if p.skills_dir] vào load_all_skills(plugin_dirs=...) (T14) và [p.hooks_file for p in plugins if p.hooks_file] vào HookExecutor (T15) để wire up toàn bộ extension ecosystem.

Tham khảo Refs

Nguồn chính