Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions docs/domains.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ since. A module "runs queries" when it imports a query builder (`select`,
| Service packages per domain | 6: `services/tools/`, which holds the built-in tool registry and no service yet, `services/overview/`, `services/budgets/`, `services/api_keys/`, `services/files/` and `services/providers/`, which holds the organization-scoped half of providers. `services/mail/`, `services/routing/` and `services/tenancy/` are older subpackages |
| Repository packages per domain | 6: `repositories/overview/`, `repositories/api_keys/`, `repositories/files/`, `repositories/budgets/`, `repositories/pricing/` and `repositories/providers/`. `repositories/tenancy/` is an older subpackage |
| Modules in `schemas/` | Four domain modules so far, `budgets.py`, `files.py`, `overview.py` and `providers.py` |
| Modules in `exceptions/` | The shared error bases in `_base.py`, which the package root re-exports, and three domain modules so far, `budget_exceptions.py`, `files_exceptions.py` and `providers_exceptions.py`. `services/tenancy/errors.py` holds the rest of the tenancy errors in 1,145 lines |
| Modules in `exceptions/` | The shared error bases in `_base.py`, which the package root re-exports, `shared_exceptions.py` for the errors no one domain owns, and nine domain modules: `budget_exceptions.py`, `feedback_exceptions.py`, `files_exceptions.py`, `guardrails_exceptions.py`, `identity_exceptions.py`, `organizations_exceptions.py`, `pricing_exceptions.py`, `providers_exceptions.py` and `tools_exceptions.py` |

## The domains

Expand Down Expand Up @@ -102,6 +102,7 @@ account administration.
`tenancy/password_reset_email.py`, `oauth_service.py`, `password_service.py`,
`dashboard_session_service.py`
- Repositories: `tenancy/user_repository.py`
- Exceptions: `identity_exceptions.py`

Its tables sit in `models/tenancy.py` today, which organizations holds.

Expand All @@ -116,12 +117,13 @@ provisioning, the setup guide, and the gateway's billing users.
`tenancy/authorization.py`, `tenancy/invitation_email.py`,
`tenancy/organization_domain_service.py`, `tenancy/domain_verification.py`,
`tenancy/provisioning_service.py`, `tenancy/workspace_activation_service.py`,
`tenancy/errors.py`, `workspace_scope.py`
`workspace_scope.py`
- Repositories: `tenancy/organization_repository.py`,
`tenancy/organization_member_repository.py`,
`tenancy/organization_domain_repository.py`,
`tenancy/invitation_repository.py`, `tenancy/workspace_repository.py`,
`users_repository.py`
- Exceptions: `organizations_exceptions.py`
- Models: `tenancy.py`, `users.py`

### api-keys
Expand Down Expand Up @@ -158,6 +160,7 @@ snapshots.
- Services: `pricing_service.py`, `pricing_init_service.py`,
`pricing_refresh_service.py`, `organization_pricing_service.py`
- Repositories: `pricing/`
- Exceptions: `pricing_exceptions.py`
- Models: `pricing.py`, `pricing_schemas.py`

### providers
Expand Down Expand Up @@ -239,6 +242,7 @@ retrieval and code execution.
`tenancy/workspace_web_search_service.py`,
`tenancy/workspace_code_execution_policy_service.py`, `code_execution/`
- Repositories: `code_execution/`
- Exceptions: `tools_exceptions.py`
- Ports: `code_execution_port.py`
- Adapters: `code_execution_adapter.py`, `e2b_code_execution_adapter.py`
- Models: `tools.py`, `mcp.py`
Expand All @@ -259,6 +263,7 @@ configuration.
`tenancy/organization_guardrail_definition_service.py`,
`tenancy/organization_guardrail_runner.py`
- Repositories: `tenancy/organization_guardrail_definition_repository.py`
- Exceptions: `guardrails_exceptions.py`
- Models: `guardrails.py`

### agent-gates
Expand Down Expand Up @@ -339,6 +344,7 @@ Cross-cutting modules that several domains import. They stay where they are.

- Services: `url_safety.py`, `secret_box.py`, `file_extractors.py`
- Repositories: `base_repository.py`
- Exceptions: `shared_exceptions.py`
- Models: `base.py`, `money.py`, `secret_fields.py`

`file_extractors.py` turns bytes into text and holds no state. Inference
Expand Down
8 changes: 4 additions & 4 deletions src/gateway/adapters/identity_provider_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@

from sqlalchemy.ext.asyncio import AsyncSession

from gateway.models.tenancy import User
from gateway.repositories.tenancy import UserRepository
from gateway.services.tenancy.email_address import validated_email
from gateway.services.tenancy.errors import (
from gateway.exceptions.identity_exceptions import (
InvalidEmailError,
OAuthEmailNotVerifiedError,
OAuthIdentityUnknownError,
)
from gateway.models.tenancy import User
from gateway.repositories.tenancy import UserRepository
from gateway.services.tenancy.email_address import validated_email


class RosterIdentityProviderAdapter:
Expand Down
5 changes: 1 addition & 4 deletions src/gateway/api/routes/_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
cache_write_1h_tokens_of,
cache_write_tokens_of,
)
from gateway.exceptions.tools_exceptions import WorkspaceMcpServerNotFoundError, WorkspaceWebSearchDomainsExcludedError
from gateway.inflight import track_request
from gateway.log_config import logger
from gateway.metrics import REGISTRY, Histogram
Expand Down Expand Up @@ -198,10 +199,6 @@
SandboxUnavailableError,
)
from gateway.services.secret_box import SecretBoxUnavailableError, SecretDecryptionError
from gateway.services.tenancy.errors import (
WorkspaceMcpServerNotFoundError,
WorkspaceWebSearchDomainsExcludedError,
)
from gateway.services.tenancy.org_provider_key_service import cached_org_model_restriction
from gateway.services.tenancy.organization_guardrail_runner import handle as guardrail_handle
from gateway.services.tenancy.organization_guardrail_service import (
Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/auth_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
# wording it differently would tell a person the doors closed for three reasons.
from gateway.api.routes.auth_session import MAINTENANCE_MODE_REFUSAL
from gateway.core.config import OAUTH_PROVIDERS, GatewayConfig
from gateway.exceptions import TenancyError
from gateway.exceptions.identity_exceptions import OAuthNotConfiguredError
from gateway.log_config import logger
from gateway.services.dashboard_session_service import (
apply_session_cookie,
Expand All @@ -71,7 +73,6 @@
provider_label,
require_configured,
)
from gateway.services.tenancy.errors import OAuthNotConfiguredError, TenancyError
from gateway.services.tenancy.organization_domain_service import OrganizationDomainService

router = APIRouter(prefix=OAUTH_ROUTE_PREFIX, tags=["auth"])
Expand Down
2 changes: 1 addition & 1 deletion src/gateway/api/routes/auth_password.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@
from sqlalchemy.ext.asyncio import AsyncSession

from gateway.api.deps import CurrentIdentity, get_db, verify_master_key
from gateway.exceptions.identity_exceptions import CurrentPasswordRequiredError, EmailChangeNotSupportedError
from gateway.services.dashboard_session_service import SESSION_COOKIE_NAME, hash_session_token
from gateway.services.password_service import MIN_PASSWORD_LENGTH
from gateway.services.tenancy.email_address import MAX_EMAIL_LENGTH, validated_email
from gateway.services.tenancy.errors import CurrentPasswordRequiredError, EmailChangeNotSupportedError
from gateway.services.tenancy.provisioning_service import load_bootstrap_identity
from gateway.services.tenancy.user_service import set_password, update_password

Expand Down
2 changes: 1 addition & 1 deletion src/gateway/api/routes/auth_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

from gateway.api.deps import get_config, get_db, is_valid_master_key, record_auth_failure
from gateway.core.config import GatewayConfig
from gateway.exceptions.identity_exceptions import EmailNotVerifiedError, InvalidCredentialsError
from gateway.log_config import logger
from gateway.models.tenancy import User as TenancyUser
from gateway.rate_limit import RateLimiter
Expand All @@ -65,7 +66,6 @@
from gateway.services.maintenance_mode_service import is_maintenance_mode
from gateway.services.password_service import MAX_PASSWORD_BYTES
from gateway.services.tenancy.email_address import MAX_EMAIL_LENGTH
from gateway.services.tenancy.errors import EmailNotVerifiedError, InvalidCredentialsError
from gateway.services.tenancy.organization_domain_service import OrganizationDomainService
from gateway.services.tenancy.provisioning_service import ensure_bootstrap_identity
from gateway.services.tenancy.user_service import authenticate, operator_has_password
Expand Down
3 changes: 2 additions & 1 deletion src/gateway/api/routes/auth_webauthn.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
# differently would tell a person the two doors closed for different reasons.
from gateway.api.routes.auth_session import MAINTENANCE_MODE_REFUSAL
from gateway.core.config import GatewayConfig
from gateway.exceptions import TenancyError
from gateway.exceptions.identity_exceptions import PasskeysNotConfiguredError
from gateway.log_config import logger
from gateway.models.tenancy import (
MAX_WEBAUTHN_CREDENTIAL_NAME,
Expand All @@ -58,7 +60,6 @@
)
from gateway.services.maintenance_mode_service import is_maintenance_mode
from gateway.services.tenancy import webauthn_service
from gateway.services.tenancy.errors import PasskeysNotConfiguredError, TenancyError
from gateway.services.tenancy.organization_domain_service import OrganizationDomainService

router = APIRouter(prefix="/auth/webauthn", tags=["auth"])
Expand Down
4 changes: 2 additions & 2 deletions src/gateway/api/routes/organization_guardrail_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
`gateway.services.tenancy.organization_guardrail_definition_service`: resolve the
caller's identity, call the service, return its typed result. The role gate, the
catalog rules and the secret handling live there, and the domain errors it raises
carry their own statuses (see `gateway.services.tenancy.errors`), so nothing here
catches them.
carry their own statuses (see `gateway.exceptions.guardrails_exceptions`), so
nothing here catches them.

A separate surface from ``/api/v1/organizations/me/guardrails`` because the two
say different things. A definition is *what* a check is, and one of them can be
Expand Down
4 changes: 2 additions & 2 deletions src/gateway/api/routes/organization_guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
Thin composition over `gateway.services.tenancy.organization_guardrail_service`:
resolve the caller's identity, call the service, return its typed result. The
role gate and the scope rules live there, and the domain errors it raises carry
their own statuses (see `gateway.services.tenancy.errors`), so nothing here
catches them.
their own statuses (see `gateway.exceptions.guardrails_exceptions`), so nothing
here catches them.

Scoped to ``/me`` for the reason `routes/organization_pricing.py` and
`routes/organizations.py` are: a standalone deployment has exactly one
Expand Down
2 changes: 1 addition & 1 deletion src/gateway/api/routes/organization_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
)
from gateway.auth.models import hash_key, key_suffix
from gateway.core.config import GatewayConfig
from gateway.exceptions.organizations_exceptions import WorkspaceNotFoundError
from gateway.models.api_keys import APIKey
from gateway.models.tenancy import User as TenancyUser
from gateway.models.tenancy import Workspace
Expand All @@ -69,7 +70,6 @@
from gateway.services.model_access import is_allowlist_subset, validate_allowed_models
from gateway.services.tenancy import OrganizationService
from gateway.services.tenancy.authorization import resolve_workspace_in_organization
from gateway.services.tenancy.errors import WorkspaceNotFoundError
from gateway.services.workspace_scope import organization_default_workspace_id

router = APIRouter(
Expand Down
4 changes: 2 additions & 2 deletions src/gateway/api/routes/organization_pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
the caller's identity, call the service, return its typed result. The overlap
rule, the role gate, and the refusal to re-price a model the deployment supplies
the credential for all live there, and the domain errors it raises carry their
own statuses (see `gateway.services.tenancy.errors`), so nothing here catches
them.
own statuses (see `gateway.exceptions.pricing_exceptions`), so nothing here
catches them.

Scoped to ``/me`` for the same reason `routes/organizations.py` is: a request
cannot name an organization at all, because a standalone deployment has exactly
Expand Down
8 changes: 7 additions & 1 deletion src/gateway/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
"""The gateway's error classes, presented at one import path."""
"""The gateway's error classes, presented at one import path.

Each class names the HTTP status its condition maps to, and one handler
registered in `gateway.main` renders it as FastAPI's own ``{"detail": ...}``
body. A service raises a domain error and its routes stay thin, with no
try/except around each one.
"""

from gateway.exceptions._base import (
TenancyConflictError,
Expand Down
Loading
Loading