fix(presets): scope expand-prompt model resolution to owner (#3477)

* fix(presets): scope expand-prompt model resolution to owner

/api/presets/expand resolved its model endpoint with no owner, so in a
multi-user setup it could match another user's endpoint and use its URL
and decrypted api_key. Pass effective_user(request) to _resolve_model so
resolution is owner-scoped. Adds a regression test.

* fix(presets): scope teacher and audit model resolution to owner

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Alex Little <alexwilliamlittle@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Kenny Van de Maele <kenny@kvandemaele.be>
This commit is contained in:
Alex Little
2026-06-08 20:12:02 +01:00
committed by GitHub
parent ed6cc88974
commit a58f526992
5 changed files with 160 additions and 7 deletions
+6 -5
View File
@@ -229,12 +229,13 @@ portable across users / hosts.
"""
async def _call_teacher(teacher_model_spec: str, prompt: str) -> Optional[str]:
async def _call_teacher(teacher_model_spec: str, prompt: str,
owner: Optional[str] = None) -> Optional[str]:
"""Call the configured teacher endpoint with the escalation prompt."""
from src.llm_core import llm_call_async
from src.ai_interaction import _resolve_model, _TEACHER_SYSTEM_PROMPT
try:
url, model, headers = _resolve_model(teacher_model_spec)
url, model, headers = _resolve_model(teacher_model_spec, owner=owner)
except Exception as e:
logger.warning(f"teacher endpoint not resolvable ({teacher_model_spec!r}): {e}")
return None
@@ -388,7 +389,7 @@ async def escalate_and_learn(
untrusted_trace_guard=_UNTRUSTED_TRACE_GUARD,
trace=_format_trace(tool_results, agent_reply),
)
response = await _call_teacher(teacher_spec, prompt)
response = await _call_teacher(teacher_spec, prompt, owner=owner)
if not response:
return None
@@ -523,7 +524,7 @@ async def run_teacher_inline(
# Resolve teacher endpoint
try:
from src.ai_interaction import _resolve_model
teacher_url, teacher_model, teacher_headers = _resolve_model(teacher_spec)
teacher_url, teacher_model, teacher_headers = _resolve_model(teacher_spec, owner=owner)
except Exception as e:
logger.warning(f"teacher endpoint not resolvable ({teacher_spec!r}): {e}")
yield (
@@ -617,7 +618,7 @@ async def run_teacher_inline(
untrusted_trace_guard=_UNTRUSTED_TRACE_GUARD,
trace=_format_trace(captured_tool_events, teacher_text),
)
skill_response = await _call_teacher(teacher_spec, prompt)
skill_response = await _call_teacher(teacher_spec, prompt, owner=owner)
if skill_response and "NO_SKILL" in skill_response and not _extract_skill_json(skill_response):
logger.info("teacher declined to write a skill (NO_SKILL)")
yield (