mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 07:20:44 +08:00
should_apply_workspace_role_filter no longer requires at least one bound skill. With binding enabled and an empty map, the catalog shows only public workspace skills per role (matches prewarm and runtime). Update Admin copy and add regression test. Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
5.4 KiB
Python
157 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import Any
|
|
|
|
from oclaw.runtime.agents.specialists import discover_specialist_ids
|
|
|
|
SKILL_ROLE_BINDING_KEY = "skill_role_binding"
|
|
SKILL_ROLE_BINDING_ENABLED_SETTING = "AIA_SKILL_ROLE_BINDING_ENABLED"
|
|
SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING = "AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT"
|
|
|
|
|
|
def _truthy(v: str | None) -> bool:
|
|
return str(v or "").strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def ordered_specialist_ids() -> list[str]:
|
|
base = [str(k).strip().lower() for k in discover_specialist_ids() if str(k).strip()]
|
|
preferred = [x for x in ("generalist", "ops", "memory", "image", "video") if x in set(base)]
|
|
return preferred + [x for x in base if x not in set(preferred)]
|
|
|
|
|
|
def ordered_binding_roles() -> list[str]:
|
|
return ["manager", *ordered_specialist_ids()]
|
|
|
|
|
|
def skill_role_binding_enabled_env_present() -> bool:
|
|
"""True when ``AIA_SKILL_ROLE_BINDING_ENABLED`` is set in the process environment (any value)."""
|
|
return bool(str(os.getenv(SKILL_ROLE_BINDING_ENABLED_SETTING) or "").strip())
|
|
|
|
|
|
def skill_role_binding_enabled_stored(*, store: Any) -> bool:
|
|
"""SQLite/Admin value only; ignores environment (contrast :func:`skill_role_binding_enabled`)."""
|
|
try:
|
|
raw = str(store.get_setting(SKILL_ROLE_BINDING_ENABLED_SETTING) or "").strip()
|
|
except Exception:
|
|
raw = ""
|
|
return _truthy(raw) if raw else False
|
|
|
|
|
|
def skill_role_binding_enabled(*, store: Any) -> bool:
|
|
raw_env = str(os.getenv(SKILL_ROLE_BINDING_ENABLED_SETTING) or "").strip()
|
|
if raw_env:
|
|
return _truthy(raw_env)
|
|
try:
|
|
raw = str(store.get_setting(SKILL_ROLE_BINDING_ENABLED_SETTING) or "").strip()
|
|
except Exception:
|
|
raw = ""
|
|
return _truthy(raw) if raw else False
|
|
|
|
|
|
def load_skill_role_binding_dict(store: Any) -> dict[str, Any]:
|
|
try:
|
|
raw = str(store.get_setting(SKILL_ROLE_BINDING_KEY) or "").strip()
|
|
except Exception:
|
|
raw = ""
|
|
if not raw:
|
|
return {}
|
|
try:
|
|
obj = json.loads(raw)
|
|
except Exception:
|
|
return {}
|
|
return obj if isinstance(obj, dict) else {}
|
|
|
|
|
|
def mapping_has_any_skill_names(mapping: dict[str, list[str]]) -> bool:
|
|
for vals in mapping.values():
|
|
if vals and any(str(x).strip() for x in vals):
|
|
return True
|
|
return False
|
|
|
|
|
|
def normalize_skill_role_binding(
|
|
*,
|
|
mapping_raw: dict[str, Any],
|
|
valid_skill_names: set[str],
|
|
available_roles: list[str] | None = None,
|
|
) -> dict[str, list[str]]:
|
|
roles = available_roles if available_roles is not None else ordered_binding_roles()
|
|
role_set = set(roles)
|
|
out: dict[str, list[str]] = {r: [] for r in roles}
|
|
for k, v in (mapping_raw or {}).items():
|
|
rk = str(k or "").strip().lower()
|
|
if rk not in role_set:
|
|
continue
|
|
items = v if isinstance(v, list) else []
|
|
seen: set[str] = set()
|
|
for x in items:
|
|
nm = str(x or "").strip()
|
|
if not nm or nm not in valid_skill_names or nm in seen:
|
|
continue
|
|
seen.add(nm)
|
|
out[rk].append(nm)
|
|
return out
|
|
|
|
|
|
def allowed_workspace_skill_names_for_role(*, store: Any, role: str) -> set[str]:
|
|
"""Union of manager-bound skills and skills bound to the given specialist role."""
|
|
r = str(role or "").strip().lower()
|
|
if not r:
|
|
return set()
|
|
from oclaw.runtime.skills import discover_public_workspace_skill_names
|
|
|
|
public = discover_public_workspace_skill_names()
|
|
mapping = normalize_skill_role_binding(
|
|
mapping_raw=load_skill_role_binding_dict(store),
|
|
valid_skill_names=_all_installed_skill_names(store),
|
|
)
|
|
try:
|
|
raw_env = str(os.getenv(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip()
|
|
if raw_env:
|
|
inherit_mgr = _truthy(raw_env)
|
|
else:
|
|
raw = str(store.get_setting(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip()
|
|
inherit_mgr = _truthy(raw) if raw else True
|
|
except Exception:
|
|
inherit_mgr = True
|
|
mgr = {str(x).strip() for x in (mapping.get("manager") or []) if str(x).strip()} if inherit_mgr else set()
|
|
sp = {str(x).strip() for x in (mapping.get(r) or []) if str(x).strip()}
|
|
return mgr | sp | public
|
|
|
|
|
|
def _all_installed_skill_names(store: Any) -> set[str]:
|
|
from oclaw.runtime.skills import discover_workspace_skill_manifests
|
|
|
|
return {str(m.name).strip() for m in discover_workspace_skill_manifests() if str(m.name or "").strip()}
|
|
|
|
|
|
def should_apply_workspace_role_filter(*, store: Any, skill_binding_role: str | None) -> bool:
|
|
"""When role binding is enabled, always filter the workspace skill catalog by role.
|
|
|
|
Empty binding maps still apply: each role then only sees ``public`` workspace skills
|
|
(see :func:`allowed_workspace_skill_names_for_role`), not the full install tree.
|
|
"""
|
|
if not str(skill_binding_role or "").strip():
|
|
return False
|
|
if not skill_role_binding_enabled(store=store):
|
|
return False
|
|
return True
|
|
|
|
|
|
__all__ = [
|
|
"SKILL_ROLE_BINDING_KEY",
|
|
"SKILL_ROLE_BINDING_ENABLED_SETTING",
|
|
"SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING",
|
|
"skill_role_binding_enabled_env_present",
|
|
"skill_role_binding_enabled_stored",
|
|
"allowed_workspace_skill_names_for_role",
|
|
"load_skill_role_binding_dict",
|
|
"mapping_has_any_skill_names",
|
|
"normalize_skill_role_binding",
|
|
"ordered_binding_roles",
|
|
"ordered_specialist_ids",
|
|
"should_apply_workspace_role_filter",
|
|
"skill_role_binding_enabled",
|
|
]
|