T16 — Plugin manifest-based loading
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.
test,
chúng sống ở plugin:pluginA:test và plugin:pluginB:test
— không bao giờ conflict. Đây là design choice quan trọng cho plugin ecosystem lớn.
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()
{`
@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
{`
{
"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
Tương tác với các kỹ thuật khác Interaction
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'
{`
# 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
{`
# 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
{`
# ~/.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
| Harness | Plugin format | Auto-load | Namespace | Components |
|---|---|---|---|---|
| OpenHarness | plugin.json manifest | Yes — scan ~/.openharness/plugins/ + project-local | plugin:name:cmd | skills + hooks + mcp + agents |
| Claude Code | Plugin via CLI / marketplace | Yes — namespaced auto-discover | Namespaced | skills + hooks + mcp |
| Aider | N/A | N/A | N/A | N/A |
| LangGraph | Python package (pip) | No — explicit import | Python module namespace | Tools + 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
{`
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])
`}[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
- Medium — Claude Code Extensions Explained · Punchlines về plugin bundle, namespace, silent-fail pattern
- Anthropic — Claude Code Extensions docs · Plugin format, manifest fields, auto-load behavior
- HumanLayer — Harness Engineering for Coding Agents · Plugin system trong harness engineering context
- OpenCode — Plugin system docs · Plugin model tham chiếu, so sánh manifest approach
- OWASP — Path Traversal · Security background cho path resolution validation