mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:13:19 +08:00
Remove stock/image/video specialists, Gmail watcher, CocoLoop market, desktop packaging, and extra memory plugins; disable dynamic agents and hide Session Monitor / API grants / Admin Audit from nav. Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
5.4 KiB
Python
188 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from runtime.hooks.hook_types import HookEligibilityContext
|
|
from runtime.hooks.merge_skill_hook_dirs import merge_skill_hook_extra_dirs_into_config
|
|
from svc.config.paths import PROJECT_ROOT
|
|
from svc.config.runtime_paths import runtime_hooks_bundled_root
|
|
|
|
|
|
@dataclass
|
|
class _HooksState:
|
|
initialized: bool = False
|
|
loaded_count: int = 0
|
|
last_error: str = ""
|
|
hooks_mod: Any = None
|
|
resolved_config: dict[str, Any] | None = None
|
|
|
|
|
|
_STATE = _HooksState()
|
|
|
|
|
|
def _reset_hooks_runtime_state_for_test() -> None:
|
|
_STATE.initialized = False
|
|
_STATE.loaded_count = 0
|
|
_STATE.last_error = ""
|
|
_STATE.hooks_mod = None
|
|
_STATE.resolved_config = None
|
|
|
|
|
|
def _ensure_oclaw_path() -> Path:
|
|
oclaw_dir = Path(PROJECT_ROOT).resolve()
|
|
if str(oclaw_dir) not in sys.path:
|
|
sys.path.insert(0, str(oclaw_dir))
|
|
return oclaw_dir
|
|
|
|
|
|
def _default_runtime_config() -> dict[str, Any]:
|
|
return {"hooks": {"internal": {"enabled": True}}}
|
|
|
|
|
|
def _deep_merge_dict(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
out: dict[str, Any] = dict(base)
|
|
for k, v in (patch or {}).items():
|
|
if isinstance(v, dict) and isinstance(out.get(k), dict):
|
|
out[k] = _deep_merge_dict(dict(out.get(k) or {}), v)
|
|
else:
|
|
out[k] = v
|
|
return out
|
|
|
|
|
|
def _read_json_file(path: Path) -> dict[str, Any] | None:
|
|
try:
|
|
if not path.exists() or not path.is_file():
|
|
return None
|
|
raw = path.read_text(encoding="utf-8")
|
|
obj = json.loads(raw)
|
|
return obj if isinstance(obj, dict) else None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def resolve_runtime_config() -> dict[str, Any]:
|
|
cfg = _default_runtime_config()
|
|
env_json = str(os.getenv("OCLAW_RUNTIME_CONFIG_JSON") or "").strip()
|
|
if env_json:
|
|
try:
|
|
obj = json.loads(env_json)
|
|
if isinstance(obj, dict):
|
|
cfg = _deep_merge_dict(cfg, obj)
|
|
return cfg
|
|
except Exception:
|
|
pass
|
|
|
|
raw_path = str(os.getenv("OCLAW_CONFIG_PATH") or "").strip()
|
|
if raw_path:
|
|
p = Path(raw_path).expanduser()
|
|
if not p.is_absolute():
|
|
p = (Path(PROJECT_ROOT) / p).resolve()
|
|
file_cfg = _read_json_file(p)
|
|
if isinstance(file_cfg, dict):
|
|
cfg = _deep_merge_dict(cfg, file_cfg)
|
|
return cfg
|
|
|
|
default_cfg_path = (Path(PROJECT_ROOT) / "oclaw.json").resolve()
|
|
file_cfg = _read_json_file(default_cfg_path)
|
|
if isinstance(file_cfg, dict):
|
|
cfg = _deep_merge_dict(cfg, file_cfg)
|
|
return cfg
|
|
|
|
|
|
def initialize_hooks_runtime(
|
|
*,
|
|
cfg: dict[str, Any] | None,
|
|
workspace_dir: str,
|
|
eligibility: HookEligibilityContext | None = None,
|
|
) -> int:
|
|
if _STATE.initialized:
|
|
return int(_STATE.loaded_count or 0)
|
|
try:
|
|
oclaw_dir = _ensure_oclaw_path()
|
|
from runtime import hooks as hooks_mod # type: ignore
|
|
|
|
bundled_dir = runtime_hooks_bundled_root()
|
|
resolved_cfg = merge_skill_hook_extra_dirs_into_config(dict(cfg or resolve_runtime_config() or {}))
|
|
loaded = int(
|
|
hooks_mod.load_internal_hooks(
|
|
resolved_cfg,
|
|
workspace_dir=str(workspace_dir or "."),
|
|
bundled_hooks_dir=str(bundled_dir),
|
|
eligibility=eligibility,
|
|
)
|
|
or 0
|
|
)
|
|
_STATE.initialized = True
|
|
_STATE.loaded_count = loaded
|
|
_STATE.hooks_mod = hooks_mod
|
|
_STATE.resolved_config = resolved_cfg
|
|
_STATE.last_error = ""
|
|
return loaded
|
|
except Exception as exc:
|
|
_STATE.initialized = True
|
|
_STATE.loaded_count = 0
|
|
_STATE.hooks_mod = None
|
|
_STATE.resolved_config = None
|
|
_STATE.last_error = f"{type(exc).__name__}: {exc}"
|
|
return 0
|
|
|
|
|
|
def hooks_status() -> dict[str, Any]:
|
|
return {
|
|
"initialized": bool(_STATE.initialized),
|
|
"loaded_count": int(_STATE.loaded_count or 0),
|
|
"last_error": str(_STATE.last_error or ""),
|
|
"has_config": isinstance(_STATE.resolved_config, dict),
|
|
}
|
|
|
|
|
|
def _trigger_async_or_sync(coro: Any) -> None:
|
|
if not asyncio.iscoroutine(coro):
|
|
return
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
loop = None
|
|
if loop and loop.is_running():
|
|
loop.create_task(coro)
|
|
else:
|
|
asyncio.run(coro)
|
|
|
|
|
|
def trigger_hook_event(
|
|
*,
|
|
event_type: str,
|
|
action: str,
|
|
session_key: str,
|
|
context: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
hooks_mod = _STATE.hooks_mod
|
|
if hooks_mod is None:
|
|
return {}
|
|
try:
|
|
mutable_context: dict[str, Any] = {} if context is None else context
|
|
ev = hooks_mod.create_hook_event(
|
|
str(event_type or ""),
|
|
str(action or ""),
|
|
str(session_key or "system"),
|
|
context=mutable_context,
|
|
)
|
|
_trigger_async_or_sync(hooks_mod.trigger_hook(ev))
|
|
if isinstance(getattr(ev, "context", None), dict):
|
|
return dict(getattr(ev, "context"))
|
|
return mutable_context
|
|
except Exception:
|
|
return dict(context) if context is not None else {}
|
|
|
|
|
|
def get_active_hooks_config() -> dict[str, Any]:
|
|
if isinstance(_STATE.resolved_config, dict):
|
|
return dict(_STATE.resolved_config)
|
|
return resolve_runtime_config()
|