From b5b76fe46d86ccd9c9079602df272b36dbd91826 Mon Sep 17 00:00:00 2001 From: Harshit Ojha <92013456+harshit-ojha0324@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:02:23 -0400 Subject: [PATCH] fix(skills): inject published skills when Auto-approve is off (#6643) With Auto-approve OFF the agent loop asked get_relevant_skills for min_confidence=2.0, meant as 'published only'. The gate applies the confidence check to published skills too, and confidence is 0-1, so no user skill was ever injected, published or not. The explicit-activation path already treats OFF as status == published. Add a published_only flag to get_relevant_skills and pass it from both callers that used the sentinel (relevance injection and the skill preflight). Auto-approve ON is unchanged: a skill still needs a passing audit and the confidence threshold. Fixes #6634 --- services/memory/skills.py | 13 ++++++++++--- src/agent_loop.py | 12 ++++++++---- tests/test_skill_index_toolset_gating.py | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/services/memory/skills.py b/services/memory/skills.py index 713ae2199..5bf583389 100644 --- a/services/memory/skills.py +++ b/services/memory/skills.py @@ -745,6 +745,7 @@ class SkillsManager: min_confidence: float = 0.0, available_toolsets: Optional[Iterable[str]] = None, platform: Optional[str] = None, + published_only: bool = False, ) -> List[Dict]: if skills is None: skills = self.load_all() @@ -775,9 +776,15 @@ class SkillsManager: if not skill.get("platforms") or platform in skill.get("platforms", []) ] # Prompt injection is fail-closed for user skills. Built-ins are - # shipped procedures; every other skill needs a passing audit and a - # confidence score at the user's current threshold. - if min_confidence > 0: + # shipped procedures. With Auto-approve OFF (published_only), the + # user's publish is the approval; otherwise a skill needs a passing + # audit and a confidence score at the user's current threshold. + if published_only: + skills = [ + s for s in skills + if s.get("source") == "builtin" or s.get("status") == "published" + ] + elif min_confidence > 0: def _passes(s): if s.get("source") == "builtin": return True diff --git a/src/agent_loop.py b/src/agent_loop.py index 6301e86a5..c7d1f179c 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -10325,9 +10325,10 @@ def _has_matching_skill_for_turn( )))) if max_items == 0: return False + published_only = not prefs.get("auto_approve_skills", True) min_confidence = ( - 2.0 - if not prefs.get("auto_approve_skills", True) + 0.0 + if published_only else float(prefs.get( "skill_min_confidence", get_setting("skill_autosave_min_confidence", 0.85), @@ -10344,6 +10345,7 @@ def _has_matching_skill_for_turn( threshold=0.25, max_items=max_items, min_confidence=min_confidence, + published_only=published_only, )) except Exception as exc: logger.debug("skill preflight failed (non-fatal): %s", exc) @@ -14952,8 +14954,9 @@ def _build_system_prompt( # confidence threshold. Approve OFF → published-only (no draft # passes). Approve ON → drafts at/above the chosen confidence # (0 = "All"). Falls back to the global default setting. - if not _prefs.get("auto_approve_skills", True): - _skill_min_conf = 2.0 # nothing draft clears it → published only + _skill_published_only = not _prefs.get("auto_approve_skills", True) + if _skill_published_only: + _skill_min_conf = 0.0 # published_only decides eligibility else: try: _skill_min_conf = float(_prefs.get( @@ -14976,6 +14979,7 @@ def _build_system_prompt( max_items=_skill_max_injected, min_confidence=_skill_min_conf, available_toolsets=relevant_tools, + published_only=_skill_published_only, ) if _skill_max_injected > 0 else [] else: # Explicit client activation chooses which eligible skill diff --git a/tests/test_skill_index_toolset_gating.py b/tests/test_skill_index_toolset_gating.py index 92ea8f578..8a93933ae 100644 --- a/tests/test_skill_index_toolset_gating.py +++ b/tests/test_skill_index_toolset_gating.py @@ -133,3 +133,20 @@ def test_unaudited_or_below_threshold_published_skill_is_not_injected(tmp_path): assert not sm.get_relevant_skills( "premature skill", min_confidence=0.80, ) + + +def test_auto_approve_off_injects_published_skills_without_an_audit(tmp_path): + # #6634: OFF used min_confidence=2.0, which no 0-1 confidence clears, so + # every user skill was rejected. Publishing is the approval when OFF. + (tmp_path / "skills").mkdir() + for name, status in (("premature-skill", "published"), ("draft-skill", "draft")): + path = _write_skill_md(tmp_path / "skills", name) + text = path.read_text(encoding="utf-8").replace("audit_verdict: pass\n", "") + path.write_text(text.replace("status: published", f"status: {status}"), encoding="utf-8") + sm = SkillsManager(str(tmp_path)) + + query = "premature skill draft skill" + # threshold=0 isolates the eligibility gate from relevance scoring. + assert _names(sm.get_relevant_skills(query, threshold=0.0, published_only=True)) == {"premature-skill"} + # Auto-approve ON still needs a passing audit, as before. + assert not sm.get_relevant_skills(query, threshold=0.0, min_confidence=0.85)