Files
odysseus/tests/test_task_scheduler_endpoint_authority.py
T
Alexandre Teixeira 0133de6f0e feat(security): apply audited PR #6738 remediations and test hardening
- Enforce vision probe SSRF safe local host validation, SafeProbeBackend TOCTOU elimination, and session owner authorization
- Authorize session titling endpoints against registered owner configuration
- Sanitize sensitive metadata logging from agent loop debug logs
- Add async non-blocking execution for spec router tools and thinking decisions
- Harden email cache invalidation fencing and thread-safe mutation
- Add regression test suites for legacy browser authority, spec contracts, spec router request scoping, and task MCP delivery
2026-10-27 17:25:25 +00:00

247 lines
13 KiB
Python

"""Scheduler credential recovery uses registered endpoint identity after normalization."""
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
from urllib.parse import urlparse
import pytest
import core.database as database
import src.endpoint_resolver as resolver
from src.agent_runtime.authority import seal_task_authority
from src.task_scheduler import TaskScheduler
from tests.helpers.database import disposable_database
PROVIDERS = [
("openai", "https://api.openai.com", "https://api.openai.com/v1/chat/completions"),
("anthropic_v1", "https://api.anthropic.com/v1", "https://api.anthropic.com/v1/messages"),
("ollama", "http://192.168.1.5:11434", "http://192.168.1.5:11434/api/chat"),
("generic", "https://models.example.test/v1", "https://models.example.test/v1/chat/completions"),
("anthropic_root", "https://api.anthropic.com", "https://api.anthropic.com/v1/messages"),
("ollama_api", "http://192.168.1.5:11434/api", "http://192.168.1.5:11434/api/chat"),
]
@pytest.fixture
def execution(tmp_path, monkeypatch):
with disposable_database(tmp_path) as factory:
monkeypatch.setattr(database, "SessionLocal", factory)
monkeypatch.setattr(resolver, "resolve_url", lambda url: url)
monkeypatch.setattr("src.interactive_gate.wait_for_interactive_quiet", AsyncMock())
monkeypatch.setattr("src.task_endpoint.resolve_task_candidates", lambda **kwargs: [])
monkeypatch.setattr("src.settings.get_setting", lambda key, default=None: default)
monkeypatch.setattr("src.task_scheduler._resolve_task_timezone", lambda db, task: "UTC")
monkeypatch.setattr("src.tool_index.get_tool_index", lambda: None)
monkeypatch.setattr("src.research_handler.RESEARCH_DATA_DIR", tmp_path / "research")
monkeypatch.setattr("src.event_bus.fire_event", lambda *args: None)
captured = {}
async def stream(**kwargs):
captured.update(endpoint_url=kwargs["endpoint_url"], headers=kwargs["headers"])
yield 'data: {"delta": "Complete"}\n\n'
monkeypatch.setattr("src.agent_loop.stream_agent_loop", stream)
researcher = SimpleNamespace(research=AsyncMock(return_value="Complete"),
get_stats=lambda: {}, findings=[])
def research(**kwargs):
captured.update(endpoint_url=kwargs["llm_endpoint"], headers=kwargs["llm_headers"])
return researcher
monkeypatch.setattr("src.deep_research.DeepResearcher", research)
async def run(kind, url, *, execute_agent_task=True, **overrides):
task = SimpleNamespace(
id="task", owner="alice", name="Research task", prompt="Research this topic",
task_type="research" if kind == "research" else "llm", action=None,
endpoint_url=url, model="model", session_id="session", max_steps=1,
crew_member_id=None, character_id=None,
headers={"Authorization": "Bearer caller-secret", "x-api-key": "caller-secret"},
)
task.__dict__.update(overrides)
task.request_authority_json = seal_task_authority(
task.prompt, task.task_type, task.action, owner=task.owner)
scheduler = TaskScheduler.__new__(TaskScheduler)
scheduler._session_manager = None
if kind == "agent":
if execute_agent_task:
with factory() as db:
result = await scheduler._execute_llm_task(task, db)
else:
result = await scheduler._run_agent_loop(url, task.model, task, task.session_id)
else:
with factory() as db:
result = await scheduler._execute_research_task(task, db)
assert result == "Complete"
return captured
yield factory, run
@pytest.mark.parametrize("kind", ["agent", "research"])
@pytest.mark.parametrize("provider, base, chat_url", PROVIDERS)
@pytest.mark.parametrize("selection", ["base", "chat"])
@pytest.mark.parametrize("server_key", ["server-secret", None])
async def test_scheduler_recovers_static_credentials_after_chat_normalization(execution, kind, provider, base, chat_url, selection, server_key):
factory, run = execution
with factory() as db:
db.add(database.ModelEndpoint(id="endpoint", name="Endpoint", owner="alice",
base_url=base, api_key=server_key, is_enabled=True))
db.commit()
captured = await run(kind, base if selection == "base" else chat_url)
assert captured == {"endpoint_url": chat_url, "headers": resolver.build_headers(server_key, base)}
@pytest.mark.parametrize("kind", ["agent", "research"])
@pytest.mark.parametrize("provider, base, chat_url", PROVIDERS)
async def test_scheduler_refreshes_registered_session_credentials(execution, monkeypatch, kind, provider, base, chat_url):
factory, run = execution
with factory() as db:
db.add(database.ModelEndpoint(id="endpoint", name="Endpoint", owner="alice",
base_url=base, api_key="stale-secret", is_enabled=True,
provider_auth_id="server-session"))
db.commit()
runtime = MagicMock(return_value={"base_url": base, "api_key": "refreshed-secret"})
monkeypatch.setattr("src.chatgpt_subscription.resolve_runtime_credentials", runtime)
captured = await run(kind, base)
runtime.assert_called_once_with("server-session", owner="alice")
assert captured == {"endpoint_url": chat_url, "headers": resolver.build_headers("refreshed-secret", base)}
@pytest.mark.parametrize("kind", ["agent", "research"])
async def test_scheduler_uses_refreshed_server_runtime_url(execution, monkeypatch, kind):
factory, run = execution
base = "https://chatgpt.com/backend-api/codex"
runtime_base = "https://runtime.example.test/v1"
with factory() as db:
db.add(database.ModelEndpoint(id="endpoint", name="Endpoint", owner="alice",
base_url=base, api_key="stale-secret", is_enabled=True,
provider_auth_id="server-session"))
db.commit()
runtime = MagicMock(return_value={"base_url": runtime_base, "api_key": "refreshed-secret"})
monkeypatch.setattr("src.chatgpt_subscription.resolve_runtime_credentials", runtime)
captured = await run(kind, base)
runtime.assert_called_once_with("server-session", owner="alice")
assert captured == {"endpoint_url": runtime_base + "/chat/completions",
"headers": {"Authorization": "Bearer refreshed-secret"}}
@pytest.mark.parametrize("kind", ["agent", "research"])
async def test_scheduler_refresh_failure_never_uses_static_or_caller_credentials(execution, monkeypatch, kind):
factory, run = execution
base = PROVIDERS[0][1]
with factory() as db:
db.add(database.ModelEndpoint(id="endpoint", name="Endpoint", owner="alice",
base_url=base, api_key="stale-secret", is_enabled=True,
provider_auth_id="server-session"))
db.commit()
runtime = MagicMock(side_effect=ValueError("Session expired"))
monkeypatch.setattr("src.chatgpt_subscription.resolve_runtime_credentials", runtime)
fallback = AsyncMock(side_effect=AssertionError("unauthorized endpoint must not fall back"))
monkeypatch.setattr("src.task_endpoint.task_llm_call_async", fallback)
with pytest.raises((ValueError, RuntimeError)):
await run(kind, base)
fallback.assert_not_awaited()
runtime.assert_called_once_with("server-session", owner="alice")
@pytest.mark.parametrize("kind", ["agent", "research"])
@pytest.mark.parametrize("provider, base, chat_url", PROVIDERS[:4])
@pytest.mark.parametrize("state", ["disabled", "foreign_owner", "unregistered", "shared"])
async def test_scheduler_credentials_require_enabled_visible_registration(execution, monkeypatch, kind, provider, base, chat_url, state):
factory, run = execution
with factory() as db:
if state != "unregistered":
db.add(database.ModelEndpoint(id="endpoint", name="Endpoint",
owner="bob" if state == "foreign_owner" else None if state == "shared" else "alice",
base_url=base, api_key="server-secret", is_enabled=state != "disabled"))
db.commit()
runtime = MagicMock(wraps=resolver.resolve_endpoint_runtime)
monkeypatch.setattr(resolver, "resolve_endpoint_runtime", runtime)
if state == "shared":
captured = await run(kind, base)
assert captured["headers"] == resolver.build_headers("server-secret", base)
assert runtime.call_args.kwargs == {"owner": "alice"}
else:
with pytest.raises((ValueError, RuntimeError)):
await run(kind, base)
runtime.assert_not_called()
@pytest.mark.parametrize("kind", ["agent", "research"])
@pytest.mark.parametrize("provider, base, chat_url", PROVIDERS[:4])
@pytest.mark.parametrize("change", [
"scheme", "host", "port", "userinfo", "query", "fragment",
"sibling", "descendant", "models", "responses",
])
async def test_scheduler_invalid_route_never_recovers_registered_credentials(execution, monkeypatch, kind, provider, base, chat_url, change):
factory, run = execution
with factory() as db:
db.add(database.ModelEndpoint(id="endpoint", name="Endpoint", owner="alice",
base_url=base, api_key="server-secret", is_enabled=True))
db.commit()
parsed = urlparse(chat_url)
mutations = {
"scheme": parsed._replace(scheme="https" if parsed.scheme == "http" else "http"),
"host": parsed._replace(netloc="attacker.example.test"),
"port": parsed._replace(netloc=f"{parsed.hostname}:4444"),
"userinfo": parsed._replace(netloc=f"caller:secret@{parsed.netloc}"),
"query": parsed._replace(query="api_key=caller-secret"),
"fragment": parsed._replace(fragment="fragment"),
"sibling": parsed._replace(path=urlparse(base).path + "/sibling"),
"descendant": parsed._replace(path=parsed.path + "/descendant"),
"models": parsed._replace(path=urlparse(base).path + "/models"),
"responses": parsed._replace(path=urlparse(base).path + "/responses"),
}
runtime = MagicMock(wraps=resolver.resolve_endpoint_runtime)
monkeypatch.setattr(resolver, "resolve_endpoint_runtime", runtime)
with pytest.raises((ValueError, RuntimeError)):
await run(kind, mutations[change].geturl(), execute_agent_task=False)
runtime.assert_not_called()
async def test_scheduled_research_keeps_configured_resolver_credentials(execution, monkeypatch):
_, run = execution
url = "https://runtime.example.test/v1/chat/completions"
headers = {"Authorization": "Bearer configured-server-secret"}
configured = MagicMock(return_value=(url, "model", headers))
monkeypatch.setattr(resolver, "resolve_endpoint", configured)
recovery = MagicMock(side_effect=AssertionError("Configured credentials must not be replaced"))
monkeypatch.setattr(resolver, "resolve_owner_registered_endpoint", recovery)
captured = await run("research", None)
assert captured == {"endpoint_url": url, "headers": headers}
assert configured.call_args.kwargs["owner"] == "alice"
recovery.assert_not_called()
@pytest.mark.parametrize('kind', ['agent', 'research'])
async def test_scheduler_refuses_unregistered_task_endpoint(execution, kind):
_, run = execution
with pytest.raises((ValueError, RuntimeError)):
await run(kind, 'https://unregistered.example.test/v1')
@pytest.mark.parametrize('kind', ['agent', 'research'])
async def test_ownerless_task_cannot_use_another_owners_endpoint(execution, monkeypatch, kind):
factory, run = execution
monkeypatch.setenv('AUTH_ENABLED', 'true')
with factory() as db:
db.add(database.ModelEndpoint(id='bob', name='Bob endpoint', owner='bob',
base_url='https://bob.example.test/v1', is_enabled=True))
db.commit()
with pytest.raises((ValueError, RuntimeError)):
await run(kind, 'https://bob.example.test/v1', owner=None)
@pytest.mark.parametrize('kind', ['agent', 'research'])
async def test_ownerless_task_works_in_explicit_single_user_mode(execution, monkeypatch, kind):
factory, run = execution
monkeypatch.setenv('AUTH_ENABLED', 'false')
with factory() as db:
db.add(database.ModelEndpoint(id='shared', name='Local endpoint', owner=None,
base_url='http://localhost:1234/v1', is_enabled=True))
db.commit()
captured = await run(kind, 'http://localhost:1234/v1', owner=None)
assert captured['endpoint_url'] == 'http://localhost:1234/v1/chat/completions'