feat(N1): Scope-Registry via Contract — workspace_scopes() Deklarationen + /scope-definitions Endpoint (#365)
Check Cross-Plugin Imports / check (push) Has been cancelled

- 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
This commit is contained in:
Agent Zero
2026-08-31 23:17:54 +02:00
parent 6ed4bb7f98
commit c25356c257
10 changed files with 652 additions and 3 deletions
+69 -1
View File
@@ -2,7 +2,9 @@
from __future__ import annotations
from pydantic import BaseModel
from typing import Literal
from pydantic import BaseModel, Field, field_validator, model_validator
class WorkspaceBase(BaseModel):
@@ -34,6 +36,72 @@ class WorkspaceResponse(WorkspaceBase):
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