Files
devplacepy/tests/unit/services/devii/actions/dispatcher.py
T
retoorandClaude Sonnet 5 569f1dcc64 Add OpenCode Zen support, model health/stats dashboard, and gateway fallback fixes
AI gateway:
- Add a generic, admin-selectable `client_profile` field on gateway_providers
  (e.g. "opencode") so a provider needing special request headers (OpenCode
  Zen's client-identity spoofing) is configured like any other provider, not
  hardcoded by name.
- Track per-(provider, model) reliability/speed/latency health in memory,
  seeded from the existing gateway_usage_ledger at startup - purely
  observational, never influences routing.
- New Stats tab on /admin/gateway: request volume, latency, per-model
  breakdowns, and reliability weight, charted with a vendored Chart.js and
  devplace's own theme tokens.
- Record which model a failed request actually fell back to
  (fallback_used_route), surfaced in the Recent Failures table.
- Stop excluding context_length errors from fallback, and skip a primary
  attempt outright when its known context window is already too small for
  the estimated request size, going straight to the fallback.
- gateway_usage_ledger's provider/fallback_used_route columns and indexes
  are ensured centrally in database/schema.py's init_db(), the single point
  of truth for this table's schema.
- Non-OpenAI upstream routing and client-model passthrough; trust only the
  upstream's own X-Gateway-Model header for served-model attribution.

Devii agent:
- Fix a real lockup: plan/verify tools could be individually disabled via
  the admin tool toggles while still being required by the protocol gate,
  permanently bricking any task that needed tools. They can no longer be
  disabled, and the gate now also checks the tool is actually offered.
- Fix compaction being silently calibrated for a 1M-token model while
  running a much smaller one: context budget is now percentage-based and
  the summarizer's own request is sized to fit the real model.
- Give a specific, actionable retry message when plan()'s own arguments get
  cut off by the output limit, and tighten its schema to discourage
  overlong plans.

Other:
- Backup service: offload completed backups to a remote Hetzner Storage Box.
- Container manager: fix orphan blob leaks from sync races, add a two-phase
  plan/execute `system prune` CLI command.
- Admin gateway UI: replace the JS-rendered model/provider tables with
  server-rendered forms and pages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhmEkvutuwtzFVcLbTrhdo
2026-09-09 07:38:24 +02:00

137 lines
4.6 KiB
Python

# retoor <retoor@molodetz.nl>
import json
from tests.conftest import run_async
from devplacepy.services.devii.actions.catalog import PLATFORM_CATALOG
from devplacepy.services.devii.actions.dispatcher import Dispatcher
class _Recorder:
def __init__(self):
self.calls = []
def record_system(self, event_key, **kwargs):
self.calls.append((event_key, kwargs))
return "rec"
class _FakeClient:
def __init__(self, authenticated=True):
self.authenticated = authenticated
self.username = "voidwalker"
def _bare_dispatcher(owner_kind, owner_id, *, is_admin=False, is_primary_admin=False):
dispatcher = Dispatcher.__new__(Dispatcher)
dispatcher._actions = PLATFORM_CATALOG.by_name()
dispatcher._virtual_tools = None
dispatcher._client = _FakeClient(authenticated=True)
dispatcher._is_admin = is_admin
dispatcher._is_primary_admin = is_primary_admin
dispatcher._owner_kind = owner_kind
dispatcher._owner_id = owner_id
return dispatcher
def _patch_recorder(monkeypatch):
from devplacepy.services.audit import record as audit_record
recorder = _Recorder()
monkeypatch.setattr(audit_record, "record_system", recorder.record_system)
return recorder
def test_admin_tool_denied_for_member_is_audited(monkeypatch):
recorder = _patch_recorder(monkeypatch)
dispatcher = _bare_dispatcher("user", "member-uid-123", is_admin=False)
result = run_async(
dispatcher.dispatch(
"admin_set_user_role", {"username": "voidwalker", "role": "Admin"}
)
)
assert json.loads(result)["error"] == "auth_required"
assert len(recorder.calls) == 1
event_key, kwargs = recorder.calls[0]
assert event_key == "security.authz.denied"
assert kwargs["origin"] == "devii"
assert kwargs["via_agent"] == 1
assert kwargs["result"] == "denied"
assert kwargs["actor_kind"] == "user"
assert kwargs["actor_uid"] == "member-uid-123"
assert kwargs["actor_role"] == "member"
assert kwargs["metadata"]["tool"] == "admin_set_user_role"
def test_admin_tool_denied_for_guest_actor_shape(monkeypatch):
recorder = _patch_recorder(monkeypatch)
dispatcher = _bare_dispatcher("guest", "cookie-abc", is_admin=False)
run_async(dispatcher.dispatch("admin_list_users", {}))
assert len(recorder.calls) == 1
_, kwargs = recorder.calls[0]
assert kwargs["actor_kind"] == "guest"
assert kwargs["actor_uid"] is None
assert kwargs["actor_role"] == "guest"
def test_admin_tool_allowed_for_admin_is_not_denial_audited(monkeypatch):
recorder = _patch_recorder(monkeypatch)
dispatcher = _bare_dispatcher("user", "admin-uid-9", is_admin=True)
run_async(dispatcher.dispatch("admin_list_users", {}))
assert all(call[0] != "security.authz.denied" for call in recorder.calls)
def test_primary_admin_tool_denied_for_regular_admin_is_audited(monkeypatch):
recorder = _patch_recorder(monkeypatch)
dispatcher = _bare_dispatcher(
"user", "admin-uid-9", is_admin=True, is_primary_admin=False
)
result = run_async(dispatcher.dispatch("db_list_tables", {}))
assert json.loads(result)["error"] == "auth_required"
assert len(recorder.calls) == 1
event_key, kwargs = recorder.calls[0]
assert event_key == "security.authz.denied"
assert kwargs["metadata"]["tool"] == "db_list_tables"
assert kwargs["actor_role"] == "admin"
def test_admin_disabled_tool_is_refused_even_for_admin(monkeypatch):
from devplacepy.services.devii import tool_prefs
recorder = _patch_recorder(monkeypatch)
monkeypatch.setattr(
tool_prefs, "disabled_tool_names", lambda: frozenset({"create_post"})
)
dispatcher = _bare_dispatcher("user", "admin-uid-9", is_admin=True, is_primary_admin=True)
result = run_async(dispatcher.dispatch("create_post", {"content": "hello"}))
payload = json.loads(result)
assert payload["error"] == "tool_disabled"
assert len(recorder.calls) == 1
event_key, kwargs = recorder.calls[0]
assert event_key == "security.authz.denied"
assert kwargs["metadata"]["tool"] == "create_post"
assert kwargs["metadata"]["reason"] == "disabled by administrator"
def test_non_disabled_tool_unaffected_by_disabled_set(monkeypatch):
from devplacepy.services.devii import tool_prefs
monkeypatch.setattr(
tool_prefs, "disabled_tool_names", lambda: frozenset({"create_post"})
)
dispatcher = _bare_dispatcher("user", "admin-uid-9", is_admin=True, is_primary_admin=True)
result = run_async(dispatcher.dispatch("admin_list_users", {}))
assert json.loads(result).get("error") != "tool_disabled"