feat(skills): remote-only tag hides skills from local endpoints (#6693)

Skills tagged remote-only in frontmatter are excluded from the injected
skill index when the active model's endpoint is local (loopback/private/
tailscale per is_local_endpoint). Big on-demand reference lookups
overwhelm small local contexts; local models never see the skill, while
remote models and API/UI listings are unaffected.

- SkillsManager.index_for gains optional keyword-only local_model flag
  (None = don't gate, same contract as active_toolsets=None)
- _build_system_prompt/_build_base_prompt thread endpoint_url from
  _build_route_request_state; unknown endpoint fails open
- tests: cover hidden-on-local, visible-on-remote/unknown, untagged
  skills never gated

Refs: discussed for the local-model overwhelm problem

Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
This commit is contained in:
zai184
2026-10-13 23:15:21 +01:00
committed by GitHub
co-authored by Alexandre Teixeira
parent f902be18f7
commit 86b886c611
3 changed files with 114 additions and 1 deletions
@@ -0,0 +1,98 @@
"""index_for() local-model gating: a `remote-only` tag must hide a skill
from local/private-endpoint models while staying visible to remote API
models and to callers that don't know the endpoint at all.
Big on-demand reference lookups overwhelm small local contexts, so the
skill must vanish from the injected index there. local_model=None (API
listings, chat preface) means "don't gate" — same contract as
active_toolsets=None.
"""
import sys
from pathlib import Path
from unittest.mock import MagicMock
# ── module-load stubbing (matches other tests in this repo) ──────────
for _mod in ("sqlalchemy", "sqlalchemy.orm", "sqlalchemy.ext", "sqlalchemy.ext.declarative"):
if _mod not in sys.modules:
try:
__import__(_mod)
except ImportError:
sys.modules[_mod] = MagicMock()
from services.memory.skills import SkillsManager # noqa: E402
def _write_skill_md(skills_root: Path, name: str, *, tags: str = "[]",
owner: str = "admin") -> None:
skill_dir = skills_root / "general" / name
skill_dir.mkdir(parents=True, exist_ok=True)
fm = "\n".join([
"---",
f"name: {name}",
"description: test skill",
"version: 1.0.0",
"category: general",
f"tags: [{tags}]",
"status: published",
"confidence: 0.9",
"audit_verdict: pass",
"source: learned",
f"owner: {owner}",
"created: 2026-01-01T00:00:00Z",
"---",
"",
"## When to Use",
"- test",
"",
"## Procedure",
"1. step 1",
"",
])
(skill_dir / "SKILL.md").write_text(fm, encoding="utf-8")
def _names(idx):
return {s["name"] for s in idx}
def test_remote_only_hidden_from_local_model(tmp_path):
(tmp_path / "skills").mkdir()
_write_skill_md(tmp_path / "skills", "agents-map", tags="reference, remote-only")
sm = SkillsManager(str(tmp_path))
sm.set_audit("agents-map", "pass", owner="admin")
assert "agents-map" not in _names(
sm.index_for(owner="admin", local_model=True))
def test_remote_only_visible_to_remote_model_and_unknown_endpoint(tmp_path):
(tmp_path / "skills").mkdir()
_write_skill_md(tmp_path / "skills", "agents-map", tags="reference, remote-only")
sm = SkillsManager(str(tmp_path))
sm.set_audit("agents-map", "pass", owner="admin")
assert "agents-map" in _names(sm.index_for(owner="admin", local_model=False))
# Unknown endpoint (background callers without a URL) fails open.
assert "agents-map" in _names(sm.index_for(owner="admin", local_model=None))
assert "agents-map" in _names(sm.index_for(owner="admin"))
def test_untagged_skills_are_never_gated(tmp_path):
(tmp_path / "skills").mkdir()
_write_skill_md(tmp_path / "skills", "plain-skill", tags="reference")
sm = SkillsManager(str(tmp_path))
sm.set_audit("plain-skill", "pass", owner="admin")
assert "plain-skill" in _names(sm.index_for(owner="admin", local_model=True))
assert "plain-skill" in _names(sm.index_for(owner="admin", local_model=False))
def test_tag_in_ui_listings_is_not_gated(tmp_path):
(tmp_path / "skills").mkdir()
_write_skill_md(tmp_path / "skills", "agents-map", tags="reference, remote-only")
sm = SkillsManager(str(tmp_path))
sm.set_audit("agents-map", "pass", owner="admin")
# API listings / UI pass no local_model → must still see the skill.
assert "agents-map" in _names(sm.index_for(owner="admin", local_model=None))