oclaw/runtime/hooks/bundled/bootstrap-extra-files/handler.py
oliver 4a23b715a2 重构仓库目录为统一的 runtime 分层并清理历史 openclaw 残留。
本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。

Made-with: Cursor
2026-04-25 01:24:23 +08:00

146 lines
4 KiB
Python

from __future__ import annotations
import fnmatch
from pathlib import Path
from typing import Any, Dict, Iterable, List, Sequence
HOOK_KEY = "bootstrap-extra-files"
_ALLOWED_BASENAMES = {
"AGENTS.md",
"SOUL.md",
"TOOLS.md",
"IDENTITY.md",
"USER.md",
"HEARTBEAT.md",
"BOOTSTRAP.md",
"MEMORY.md",
"memory.md",
}
def _resolve_hook_cfg(cfg: Any) -> Dict[str, Any]:
if not isinstance(cfg, dict):
return {}
hooks = cfg.get("hooks") if isinstance(cfg.get("hooks"), dict) else {}
internal = hooks.get("internal") if isinstance(hooks.get("internal"), dict) else {}
entries = internal.get("entries") if isinstance(internal.get("entries"), dict) else {}
row = entries.get(HOOK_KEY)
return row if isinstance(row, dict) else {}
def _string_list(v: Any) -> List[str]:
if isinstance(v, list):
out = []
for x in v:
s = str(x or "").strip()
if s:
out.append(s)
return out
if isinstance(v, str):
s = v.strip()
return [s] if s else []
return []
def _patterns(hook_cfg: Dict[str, Any]) -> List[str]:
for k in ("paths", "patterns", "files"):
got = _string_list(hook_cfg.get(k))
if got:
return got
return []
def _is_within(root: Path, candidate: Path) -> bool:
try:
candidate.resolve().relative_to(root.resolve())
return True
except Exception:
return False
def _glob_paths(root: Path, patterns: Sequence[str]) -> List[Path]:
"""
We intentionally do not use Path.glob on arbitrary patterns that might escape roots via '..'.
Instead: enumerate candidates by rglob and fnmatch on posix-style relative paths.
"""
if not root.exists() or not root.is_dir():
return []
# Pre-normalize patterns to forward-slash for fnmatch
raw_pats = [p.replace("\\", "/").lstrip("/") for p in patterns if str(p or "").strip()]
pats: list[str] = []
for pat in raw_pats:
pats.append(pat)
# Python's fnmatch doesn't treat "**/" as "zero-or-more directories".
# Add a compatibility variant so "**/AGENTS.md" matches "AGENTS.md" as well.
if pat.startswith("**/") and len(pat) > 3:
pats.append(pat[3:])
if not pats:
return []
out: List[Path] = []
try:
for p in root.rglob("*"):
if not p.is_file():
continue
if p.name not in _ALLOWED_BASENAMES:
continue
rel = p.relative_to(root).as_posix()
if any(fnmatch.fnmatch(rel, pat) for pat in pats):
out.append(p)
except Exception:
return out
# deterministic order
out.sort(key=lambda x: x.as_posix())
return out
def handle(event: Any) -> None:
if getattr(event, "type", None) != "agent" or getattr(event, "action", None) != "bootstrap":
return
ctx = getattr(event, "context", None)
if not isinstance(ctx, dict):
return
hook_cfg = _resolve_hook_cfg(ctx.get("cfg"))
if hook_cfg.get("enabled") is False:
return
patterns = _patterns(hook_cfg)
if not patterns:
return
ws = ctx.get("workspaceDir")
if not isinstance(ws, str) or not ws.strip():
return
ws_root = Path(ws).expanduser()
if not ws_root.exists() or not ws_root.is_dir():
return
matches = _glob_paths(ws_root, patterns)
if not matches:
return
# Mutate context.bootstrapFiles (Oclaw-style).
boot = ctx.get("bootstrapFiles")
if not isinstance(boot, list):
boot = []
ctx["bootstrapFiles"] = boot
existing_paths = set()
for it in list(boot):
if isinstance(it, dict) and isinstance(it.get("path"), str):
existing_paths.add(it["path"])
elif isinstance(it, str):
existing_paths.add(it)
for p in matches:
ap = str(p.resolve())
if ap in existing_paths:
continue
boot.append({"path": ap, "name": p.name})
existing_paths.add(ap)