From 091a5626e70f1e4ca611c3a1d08f65a3c8afc9ba Mon Sep 17 00:00:00 2001 From: oliver Date: Mon, 11 May 2026 15:59:51 +0800 Subject: [PATCH] Expose skill role binding stored vs env override in Admin API Add enabled_stored and enabled_env_present on skills binding GET/POST so UIs can explain when AIA_SKILL_ROLE_BINDING_ENABLED overrides SQLite. Add Admin hints for save-required and env override. Add skill_role_binding_enabled_stored/env_present helpers and a unit test. Co-authored-by: Cursor --- interfaces/admin/skills_api.py | 6 ++++ interfaces/admin/static/app.js | 35 ++++++++++++++++++++++++ runtime/skill_role_binding.py | 16 +++++++++++ tests/test_skill_role_binding_catalog.py | 13 +++++++++ 4 files changed, 70 insertions(+) diff --git a/interfaces/admin/skills_api.py b/interfaces/admin/skills_api.py index 2231d1bf..51010085 100644 --- a/interfaces/admin/skills_api.py +++ b/interfaces/admin/skills_api.py @@ -27,6 +27,8 @@ from oclaw.runtime.skill_role_binding import ( normalize_skill_role_binding, ordered_binding_roles, skill_role_binding_enabled, + skill_role_binding_enabled_env_present, + skill_role_binding_enabled_stored, ) from oclaw.runtime.skills_prompt import collect_skill_catalog_entries from oclaw.runtime.skills import _allowed_tool_names_after_wire_policy, discover_workspace_skill_manifests @@ -352,6 +354,8 @@ def include_skill_routes( return { "ok": True, "enabled": bool(skill_role_binding_enabled(store=store)), + "enabled_stored": bool(skill_role_binding_enabled_stored(store=store)), + "enabled_env_present": bool(skill_role_binding_enabled_env_present()), "manager_inherit": str(store.get_setting(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip() not in {"0", "false", "False"}, "available_roles": roles, "installed_skills": items, @@ -390,6 +394,8 @@ def include_skill_routes( return { "ok": True, "enabled": bool(skill_role_binding_enabled(store=store)), + "enabled_stored": bool(skill_role_binding_enabled_stored(store=store)), + "enabled_env_present": bool(skill_role_binding_enabled_env_present()), "manager_inherit": str(store.get_setting(SKILL_ROLE_BINDING_MANAGER_INHERIT_SETTING) or "").strip() not in {"0", "false", "False"}, "available_roles": roles, "mapping": mapping, diff --git a/interfaces/admin/static/app.js b/interfaces/admin/static/app.js index 6b395031..bec1e790 100644 --- a/interfaces/admin/static/app.js +++ b/interfaces/admin/static/app.js @@ -7792,6 +7792,15 @@ async function renderSkills() { const skillBindingState = { roles: [], names: [], mapping: {}, enabled: false, managerInherit: true }; const skillBindingStatus = el("div", { class: "muted", text: "" }); + const skillBindingPersistHint = el("div", { + class: "muted", + style: "margin:6px 0;line-height:1.5;", + text: "Checkboxes and per-role skill lists are not saved until you click Save skill role binding.", + }); + const skillBindingEnvHint = el("div", { + class: "muted", + style: "margin:6px 0;line-height:1.5;display:none;", + }); const skillEffectiveState = { items: [] }; const skillBindingEnabledCb = el("input", { type: "checkbox" }); const skillBindingManagerInheritCb = el("input", { type: "checkbox" }); @@ -7910,6 +7919,18 @@ async function renderSkills() { skillBindingState.managerInherit = Object.prototype.hasOwnProperty.call(r, "manager_inherit") ? !!r.manager_inherit : true; skillBindingEnabledCb.checked = skillBindingState.enabled; skillBindingManagerInheritCb.checked = skillBindingState.managerInherit; + if (r.enabled_env_present) { + skillBindingEnvHint.style.display = "block"; + const stored = !!r.enabled_stored; + const eff = !!r.enabled; + skillBindingEnvHint.textContent = + "Process env AIA_SKILL_ROLE_BINDING_ENABLED is set: it overrides the Admin/SQLite value at runtime. " + + "The checkbox reflects the effective (env) value. Remove or unset that variable on the oclaw process to use Admin only. " + + `(stored in DB: ${String(stored)}, effective: ${String(eff)}).`; + } else { + skillBindingEnvHint.style.display = "none"; + skillBindingEnvHint.textContent = ""; + } skillRoleSelect.innerHTML = ""; skillBindingState.roles.forEach((role) => { skillRoleSelect.appendChild(el("option", { value: role, text: role })); @@ -7942,6 +7963,18 @@ async function renderSkills() { skillBindingState.managerInherit = Object.prototype.hasOwnProperty.call(r, "manager_inherit") ? !!r.manager_inherit : true; skillBindingEnabledCb.checked = skillBindingState.enabled; skillBindingManagerInheritCb.checked = skillBindingState.managerInherit; + if (r.enabled_env_present) { + skillBindingEnvHint.style.display = "block"; + const stored = !!r.enabled_stored; + const eff = !!r.enabled; + skillBindingEnvHint.textContent = + "Process env AIA_SKILL_ROLE_BINDING_ENABLED is set: it overrides the Admin/SQLite value at runtime. " + + "The checkbox reflects the effective (env) value. Remove or unset that variable on the oclaw process to use Admin only. " + + `(stored in DB: ${String(stored)}, effective: ${String(eff)}).`; + } else { + skillBindingEnvHint.style.display = "none"; + skillBindingEnvHint.textContent = ""; + } skillBindingStatus.textContent = `saved: enabled=${String(r.enabled)} manager_inherit=${String(skillBindingState.managerInherit)}`; renderSkillBindingList(); renderSkillBindingDashboard(); @@ -7960,6 +7993,8 @@ async function renderSkills() { "When enabled, installed workspace skills only appear in the model skills catalog for the selected role. You can optionally inherit manager-bound skills to other roles.", }), skillBindingStatus, + skillBindingPersistHint, + skillBindingEnvHint, el("label", { class: "row", style: "gap:8px;align-items:center;margin-top:6px;" }, [ skillBindingEnabledCb, el("span", { text: "Enable role binding (AIA_SKILL_ROLE_BINDING_ENABLED)" }), diff --git a/runtime/skill_role_binding.py b/runtime/skill_role_binding.py index 97a3cf46..865404a4 100644 --- a/runtime/skill_role_binding.py +++ b/runtime/skill_role_binding.py @@ -25,6 +25,20 @@ 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: @@ -132,6 +146,8 @@ __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", diff --git a/tests/test_skill_role_binding_catalog.py b/tests/test_skill_role_binding_catalog.py index 2370a031..8d120c4b 100644 --- a/tests/test_skill_role_binding_catalog.py +++ b/tests/test_skill_role_binding_catalog.py @@ -8,6 +8,9 @@ from oclaw.runtime.skill_role_binding import ( SKILL_ROLE_BINDING_KEY, normalize_skill_role_binding, ordered_binding_roles, + skill_role_binding_enabled, + skill_role_binding_enabled_env_present, + skill_role_binding_enabled_stored, ) from oclaw.runtime.skills_prompt import collect_skill_catalog_entries from oclaw.runtime.skills_workspace_lane import skill_dir_private_lane_segment @@ -122,3 +125,13 @@ def test_normalize_drops_unknown_skills(tmp_path: Path) -> None: ) assert out["manager"] == ["skill-x"] assert out["generalist"] == ["skill-x"] + + +def test_skill_role_binding_env_overrides_store_value(tmp_path: Path, monkeypatch) -> None: + db = tmp_path / "rolebind.sqlite" + store = SqliteStore(str(db)) + store.set_setting(SKILL_ROLE_BINDING_ENABLED_SETTING, "1") + monkeypatch.setenv("AIA_SKILL_ROLE_BINDING_ENABLED", "0") + assert skill_role_binding_enabled_env_present() is True + assert skill_role_binding_enabled_stored(store=store) is True + assert skill_role_binding_enabled(store=store) is False