Files
Agent Zero c25356c257
Check Cross-Plugin Imports / check (push) Has been cancelled
feat(N1): Scope-Registry via Contract — workspace_scopes() Deklarationen + /scope-definitions Endpoint (#365)
- workspace_scopes() Contract-Hook (document_placeholders-Muster): Plugins deklarieren Scope-Dimensionen inkl. Wertequellen
- Deklarationen: contacts (Ordner/Typen/Saved-View), dms (Ordner/Datei-Typen), mail (Postfächer), calendar (Kalender/Standard-Ansicht)
- Pydantic fail-closed (schemas/workspace.py): ScopeOption, ScopeValueSource (nur interne /api/v1-Pfade, SSRF-sicher), WorkspaceScopeDimension, WorkspaceModuleScopes
- Aggregator workspace_scope_service.py: discovered-Plugins, ARCH-014-safe, Crash-sicher, ungültige Deklarationen verworfen
- GET /api/v1/workspaces/scope-definitions (workspaces:configure_modules) vor /{workspace_id} registriert
- Security-Invariante: Scope = reine UND-Einschränkung (Workspace ∧ RLS ∧ ABAC ∧ Permissions)
- Tests: 18/18 neu (TDD rot→grün), Regression 17/17, Checker 0 Verstöße, Ruff clean
- Doku: api-documentation.md Workspaces-Sektion, PROGRESS.md Phase N1
2026-08-31 23:17:54 +02:00

114 lines
3.1 KiB
Python

"""Schemas for Workspace API endpoints."""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, Field, field_validator, model_validator
class WorkspaceBase(BaseModel):
name: str
icon: str = "LayoutGrid"
description: str | None = None
is_default: bool = False
is_active: bool = True
class WorkspaceCreate(WorkspaceBase):
pass
class WorkspaceUpdate(BaseModel):
name: str | None = None
icon: str | None = None
description: str | None = None
is_default: bool | None = None
is_active: bool | None = None
class WorkspaceResponse(WorkspaceBase):
id: str
created_by: str | None = None
created_at: str | None = None
updated_at: str | None = None
model_config = {"from_attributes": True}
# ─── N1: Workspace-Scope-Registry (Phase N) ───────────────────
class ScopeOption(BaseModel):
"""Ein auswählbarer Wert einer Scope-Dimension."""
value: str
label: str
class ScopeValueSource(BaseModel):
"""Deklariert, woher der Editor wählbare Werte lädt.
Security (fail-closed): nur interne ``/api/v1/...``-Pfade sind
erlaubt — keine absoluten URLs, keine protokoll-relativen Pfade,
keine externen Hosts (SSRF-sicher per Konstruktion).
"""
endpoint: str
items_path: str = "items"
value_key: str = "id"
label_key: str = "name"
@field_validator("endpoint")
@classmethod
def _must_be_internal_api_path(cls, v: str) -> str:
if not v.startswith("/api/v1/"):
raise ValueError(
"value_source endpoint must be an internal /api/v1/ path"
)
return v
class WorkspaceScopeDimension(BaseModel):
"""Eine filterbare Dimension eines Moduls im Workspace (N1).
``multiselect``/``select`` brauchen zwingend ``options`` oder
``value_source`` (fail-closed gegen leere Filter-UI); ``toggle``
ist ein reiner Schalter mit ``default``.
"""
key: str = Field(..., min_length=1, max_length=100)
label: str = Field(..., min_length=1, max_length=200)
control: Literal["multiselect", "select", "toggle"]
options: list[ScopeOption] = Field(default_factory=list)
value_source: ScopeValueSource | None = None
default: bool | str | None = None
@model_validator(mode="after")
def _options_or_source_required(self) -> WorkspaceScopeDimension:
if self.control in ("multiselect", "select"):
if not self.options and self.value_source is None:
raise ValueError(
f"scope dimension '{self.key}': control '{self.control}' "
"requires options or value_source"
)
return self
class WorkspaceModuleScopes(BaseModel):
"""Scope-Contribution eines Plugins für ein Modul."""
module_key: str = Field(..., min_length=1, max_length=100)
dimensions: list[WorkspaceScopeDimension] = Field(..., min_length=1)
class WorkspaceModuleResponse(BaseModel):
id: str
workspace_id: str
module_key: str
is_visible: bool
menu_order: int
config: dict = {}
model_config = {"from_attributes": True}