from __future__ import annotations from collections.abc import Mapping, Sequence ALL_SCOPES = frozenset( { "templates:read", "templates:run", "templates:create", "templates:update", "templates:delete", "templates:apply", "templates:publish", "operations:read", "operations:execute", "jobs:read", "jobs:create", "jobs:cancel", "assets:read", "assets:write", "assets:delete", "mcp:read", "mcp:execute", "system:read", "social:accounts:read", "social:accounts:write", "social:posts:read", "social:posts:write", "social:posts:publish", "social:schedules:read", "social:schedules:write", "social:analytics:read", "analytics:read", "analytics:sync", "analytics:export", "generation:providers:read", "generation:requests:read", "generation:requests:create", "generation:jobs:cancel", "ai:read", "ai:generate", "ai:transform", "ai:analyze", "ai:create", "copilot:read", "copilot:execute", "projects:read", "projects:create", "projects:update", "projects:delete", # Frontend Architecture v2 uses this aggregate solely for capability # rendering. Enforcement remains method-specific in ScopePolicy. "projects:write", "members:read", "members:invite", "members:update", "members:remove", "teams:read", "teams:create", "teams:update", "teams:delete", "projects:share", "projects:collaborate", "comments:read", "comments:create", "approvals:read", "approvals:create", "approvals:review", "approvals:manage", "admin", } ) DEFAULT_ROLE_SCOPES: dict[str, frozenset[str]] = { "admin": frozenset({"admin"}), "developer": frozenset( { "templates:read", "templates:run", "templates:create", "templates:update", "templates:delete", "templates:apply", "templates:publish", "operations:read", "operations:execute", "jobs:read", "jobs:create", "jobs:cancel", "assets:read", "assets:write", "mcp:read", "mcp:execute", "system:read", "social:accounts:read", "social:accounts:write", "social:posts:read", "social:posts:write", "social:posts:publish", "social:schedules:read", "social:schedules:write", "social:analytics:read", "analytics:read", "analytics:sync", "analytics:export", "generation:providers:read", "generation:requests:read", "generation:requests:create", "generation:jobs:cancel", "ai:read", "ai:generate", "ai:transform", "ai:analyze", "ai:create", "copilot:read", "copilot:execute", "projects:read", "projects:create", "projects:update", "projects:delete", "projects:write", "members:read", "members:invite", "members:update", "members:remove", "teams:read", "teams:create", "teams:update", "teams:delete", "projects:share", "projects:collaborate", "comments:read", "comments:create", "approvals:read", "approvals:create", "approvals:review", "approvals:manage", } ), "operator": frozenset( { "templates:read", "templates:run", "templates:create", "templates:update", "templates:delete", "templates:apply", "templates:publish", "operations:read", "operations:execute", "jobs:read", "jobs:create", "jobs:cancel", "assets:read", "mcp:read", "mcp:execute", "system:read", "social:accounts:read", "social:accounts:write", "social:posts:read", "social:posts:write", "social:posts:publish", "social:schedules:read", "social:schedules:write", "social:analytics:read", "analytics:read", "analytics:sync", "generation:providers:read", "generation:requests:read", "generation:requests:create", "generation:jobs:cancel", "ai:read", "ai:generate", "ai:transform", "ai:analyze", "ai:create", "copilot:read", "copilot:execute", "projects:read", "projects:create", "projects:update", "projects:delete", "projects:write", "members:read", "teams:read", "projects:share", "projects:collaborate", "comments:read", "comments:create", "approvals:read", "approvals:review", } ), "viewer": frozenset( { "templates:read", "operations:read", "jobs:read", "assets:read", "mcp:read", "system:read", "social:accounts:read", "social:posts:read", "social:schedules:read", "social:analytics:read", "analytics:read", "generation:providers:read", "generation:requests:read", "ai:read", "copilot:read", "projects:read", "members:read", "teams:read", "projects:collaborate", "comments:read", "approvals:read", } ), } def configured_roles( overrides: Mapping[str, Sequence[str]] | None = None, ) -> dict[str, frozenset[str]]: roles = dict(DEFAULT_ROLE_SCOPES) for role, scopes in (overrides or {}).items(): normalized_role = role.strip().lower() normalized_scopes = frozenset(scope.strip().lower() for scope in scopes) unknown = normalized_scopes - ALL_SCOPES if unknown: raise ValueError(f"Role '{normalized_role}' has unsupported scopes: {sorted(unknown)}") roles[normalized_role] = normalized_scopes return roles def effective_scopes( role: str | None, explicit_scopes: Sequence[str], overrides: Mapping[str, Sequence[str]] | None = None, ) -> frozenset[str]: scopes = set(scope.strip().lower() for scope in explicit_scopes) if role: scopes.update(configured_roles(overrides).get(role.strip().lower(), ())) project_mutations = {"projects:create", "projects:update", "projects:delete"} if "projects:write" in scopes: scopes.update(project_mutations) if project_mutations.issubset(scopes): scopes.add("projects:write") if "admin" in scopes: return ALL_SCOPES return frozenset(scopes)