Skip to content

refactor(exceptions): split the tenancy errors into one module per domain - #1672

Merged
peteski22 merged 9 commits into
mainfrom
refactor/1193-errors-per-domain
Sep 24, 2026
Merged

peteski22 merged 9 commits into
mainfrom
refactor/1193-errors-per-domain

Conversation

@peteski22

@peteski22 peteski22 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

Every error the gateway's tenancy, identity, pricing, provider-key, tools and guardrail surfaces can raise lived in one 1,353-line file, so almost any feature touching those areas edited it: 35 commits from 7 authors in the last 90 days. This puts each error class in the exceptions module of the domain it belongs to, and removes the old file.

Nothing changes for someone running Otari. Every error keeps its class name, its message and the HTTP status it renders as, so the API answers byte for byte what it answered before. The OpenAPI spec, the Postman collection and the dashboard client are all unchanged.

The one class that is not a single domain's is SecretBoxUnavailableTenancyError, which reports an absent OTARI_SECRET_KEY. Provider keys, workspace MCP servers and both guardrail surfaces raise it, so it goes to a new exceptions/shared_exceptions.py rather than into one of those domains, which would have made the other two import that domain for it.

The issue asked for a services/tenancy/errors.py shim that re-exported for one release. That line predates #1255, which split models/entities.py and deleted the old module instead of shimming it. The same reasoning applies here and the file is deleted: gateway is a private import package rather than a published API, a second import path for every error means new code can pick either and the split never finishes, and the downstream cost turns out to be small (see below).

Open PRs that add or move an error class will conflict with this. #1659 is the only open PR that touches the old file today. The update is mechanical; see "Updating an open PR" below.

Downstream: otari-ai. The overlay names the old module in seven files. Six of them import only the status-carrying base classes, which already live in gateway.exceptions, so their fix is one line each and it removes the import side-effect their own docstrings complain about ("importing gateway.services.tenancy.errors runs its package __init__, which pulls otari's model tree and collides with the platform's on user"). The seventh is a test importing NotAuthorizedError. A matching change has to land with the otari pin bump to the first release that contains this PR. Nothing breaks before that bump, because the pin does not move on its own.

Updating an open PR

  • Put an error class in its domain's module, src/gateway/exceptions/<domain>_exceptions.py. The five status-carrying bases come from gateway.exceptions, as they already did.
  • from gateway.services.tenancy.errors import X becomes from gateway.exceptions.<domain>_exceptions import X. The table below says which module each class went to.
  • An error several domains raise goes in exceptions/shared_exceptions.py.
Module Classes
identity_exceptions.py 30: passwords, passkeys, OAuth, email verification and reset, deployment account administration
organizations_exceptions.py 31: organizations, workspaces, members, invitations, email-domain claims, provisioning
guardrails_exceptions.py 20: guardrail mandates and definitions
providers_exceptions.py 10 added, beside the 5 offered-model errors already there
tools_exceptions.py 7: workspace MCP servers, web search, sandbox code execution
pricing_exceptions.py 3: organization rate overrides
shared_exceptions.py 1: the absent secret key

Measures

Measure Before After
services/tenancy/errors.py 1,353 lines, 102 classes; 35 commits, 7 authors in 90 days removed
Largest error module errors.py, 1,353 lines identity_exceptions.py, 461 lines
Domain modules in exceptions/ 4 9, plus shared_exceptions.py

The issue's own measure, that errors.py leaves the 90-day churn list, can only be read once 90 days have passed.

How to test it locally

make lint && make typecheck && make test-unit
uv run pytest tests/integration
make openapi-check && make postman-check

The API-response criterion is covered by the existing integration suite: 521 of its tests exercise routes that raise a moved error, and they are unchanged apart from their import lines.

What was checked, on every commit as well as the tip:

  • make lint, make typecheck, make openapi-check and make postman-check pass. The spec and the collection do not move, which is the evidence that no response body changed.
  • Every class is verbatim. An AST comparison against the file on main finds all 102 classes again under exceptions/, with identical source text. The single exception is SecretBoxUnavailableTenancyError, whose docstring pointed a reader at the deleted module's docstring for why the class carries a status; it now states the reason itself. That is called out in its commit.
  • Statuses and bases are unchanged. A runtime check of all 102 classes confirms each has the same immediate base class and the same status_code as before.
  • Suites: unit 4,955 passed; integration 2,891 passed, including all 521 tests that touch a moved error.

Failing locally on this branch and on main alike, none related to this change: 20 unit tests around the master key, dashboard sessions and the hook CLI, plus test_config_env_loading and the OSS smoke gate, all from a local environment gap; and test_mcp_dependency_ceiling, which needs network egress, as AGENTS.md records. Each was re-run on main to confirm. One further hook-CLI timing test fails only under full-suite load and passes on its own; the hook CLI cannot import the gateway.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Fixes #1193

Part of #1171

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py).
  • If this changes a rule in ARCHITECTURE.md or scripts/check_architecture.py, the description names the rule and says why.

No tests were added: behavior does not change and the existing suites cover it. Test files changed only their import lines. One test was deleted, tests/unit/test_tenancy_errors_home.py, which asserted that the bases the old module re-exported were the ones in gateway.exceptions; with no re-export left there is nothing for it to compare. No rule in ARCHITECTURE.md or scripts/check_architecture.py changes: this is the layout both already describe, and the architecture check passes unchanged.

docs/domains.md gains each domain's exceptions module. Its service-module counts are left alone: the recorded figures do not reproduce from the tree at the commit they name, so decrementing one for the removed module would record a guess. Separately, the feedback domain added by #1655 has no section in that page yet, which is not this PR's to add.

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used:

An AI coding agent, run by the author.

Any additional AI details you'd like to share:

The author made the decisions: deleting the old module rather than keeping a re-export shim, after weighing it against what the issue originally asked for and what the overlay actually imports; a shared module for the one error no domain owns, rather than _base.py or the module of whichever domain raises it most; and doing every domain on one branch with a commit each, as #1255 did, rather than a branch per domain. The agent did the moves and the import rewrites, wrote the AST and runtime comparisons against main, and drafted this description.

NOTE:
When responding to reviewer questions, please respond yourself rather than copy/pasting reviewer comments into an AI and pasting back its answer. We want to discuss with you, not your AI :)

  • I am an AI Agent filling out this form (check box if true)

Summary

Moved tenancy errors into domain-specific exception modules under gateway.exceptions and updated gateway services, routes, and tests to use the new locations. Removed services/tenancy/errors.py without adding a compatibility shim.

This groups errors by domain and removes the old import path. The changes describe the error and response behavior as unchanged.

@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository: mozilla-ai/otari/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 02fb3450-97ce-4c78-a044-d8715fac7bdc

📥 Commits

Reviewing files that changed from the base of the PR and between cf5e1fa and 6d73aae.

📒 Files selected for processing (68)
  • docs/domains.md
  • src/gateway/adapters/identity_provider_adapter.py
  • src/gateway/api/routes/_pipeline.py
  • src/gateway/api/routes/auth_oauth.py
  • src/gateway/api/routes/auth_password.py
  • src/gateway/api/routes/auth_session.py
  • src/gateway/api/routes/auth_webauthn.py
  • src/gateway/api/routes/organization_guardrail_definitions.py
  • src/gateway/api/routes/organization_guardrails.py
  • src/gateway/api/routes/organization_keys.py
  • src/gateway/api/routes/organization_pricing.py
  • src/gateway/exceptions/__init__.py
  • src/gateway/exceptions/guardrails_exceptions.py
  • src/gateway/exceptions/identity_exceptions.py
  • src/gateway/exceptions/organizations_exceptions.py
  • src/gateway/exceptions/pricing_exceptions.py
  • src/gateway/exceptions/providers_exceptions.py
  • src/gateway/exceptions/shared_exceptions.py
  • src/gateway/exceptions/tools_exceptions.py
  • src/gateway/main.py
  • src/gateway/services/budgets/_organization_surface.py
  • src/gateway/services/oauth_service.py
  • src/gateway/services/organization_pricing_service.py
  • src/gateway/services/overview/overview_service.py
  • src/gateway/services/playground_service.py
  • src/gateway/services/providers/_org_provider_model_service.py
  • src/gateway/services/tenancy/authorization.py
  • src/gateway/services/tenancy/deployment_user_service.py
  • src/gateway/services/tenancy/email_address.py
  • src/gateway/services/tenancy/errors.py
  • src/gateway/services/tenancy/org_provider_key_service.py
  • src/gateway/services/tenancy/organization_domain_service.py
  • src/gateway/services/tenancy/organization_guardrail_definition_service.py
  • src/gateway/services/tenancy/organization_guardrail_service.py
  • src/gateway/services/tenancy/organization_model_access.py
  • src/gateway/services/tenancy/organization_service.py
  • src/gateway/services/tenancy/password_policy.py
  • src/gateway/services/tenancy/provisioning_service.py
  • src/gateway/services/tenancy/user_service.py
  • src/gateway/services/tenancy/webauthn_service.py
  • src/gateway/services/tenancy/workspace_activation_service.py
  • src/gateway/services/tenancy/workspace_code_execution_policy_service.py
  • src/gateway/services/tenancy/workspace_mcp_server_service.py
  • src/gateway/services/tenancy/workspace_service.py
  • src/gateway/services/tenancy/workspace_web_search_service.py
  • tests/integration/test_deployment_user_administration.py
  • tests/integration/test_invitee_membership_inbox.py
  • tests/integration/test_org_provider_key_models.py
  • tests/integration/test_org_provider_keys.py
  • tests/integration/test_organization_budgets.py
  • tests/integration/test_organization_domains.py
  • tests/integration/test_organization_guardrail_definition_service.py
  • tests/integration/test_organization_guardrails.py
  • tests/integration/test_organization_pricing_routes.py
  • tests/integration/test_tenancy_authorization.py
  • tests/integration/test_tenancy_races.py
  • tests/integration/test_workspace_activation.py
  • tests/integration/test_workspace_code_execution_policy.py
  • tests/integration/test_workspace_mcp_servers.py
  • tests/integration/test_workspace_member_budget_policies.py
  • tests/integration/test_workspace_web_search.py
  • tests/unit/test_oauth_service.py
  • tests/unit/test_organization_guardrail_definition_rules.py
  • tests/unit/test_organization_pricing_resolution.py
  • tests/unit/test_pipeline_settlement.py
  • tests/unit/test_tenancy_errors_home.py
  • tests/unit/test_web_search_narrowing.py
  • tests/unit/test_webauthn_challenge.py
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The five status-carrying bases are defined in `exceptions/_base.py` and
reach callers through the `gateway.exceptions` package root.
`services/tenancy/errors.py` re-exported them, so eleven modules named the
tenancy module for a class it does not define. Splitting that module by
domain (#1193) leaves no module there to name, and the package root also
avoids running `services/tenancy/__init__.py`, which pulls in the tenancy
service tree for a base class.

No error class moves and no status changes.
…s.py

`SecretBoxUnavailableTenancyError` reports an absent `OTARI_SECRET_KEY`,
which is a deployment's condition rather than one domain's: provider keys,
workspace MCP servers and both guardrail surfaces raise it. Splitting the
tenancy errors by domain (#1193) leaves it no domain module, and putting it
in one would make the other three import that domain for it.

Pure move: the class, its status and its message are unchanged. Its
docstring no longer points at the old module's docstring for why the class
carries a status.
The three organization pricing errors sat in the tenancy error module
because it was the only module whose classes the registered handler
renders (#1193). `exceptions/` is now that home, so they move to their own
domain's module.

Pure move: the classes, their statuses and their messages are unchanged.
The comment explaining why pricing errors lived under tenancy is dropped,
since it no longer describes where they are.
…eptions.py

The ten provider-key errors join the offered-model errors already in the
providers module (#1193), key errors first, since a model is offered on a
key. The module gains an `__all__`, which the split would otherwise drop
for the ten names the tenancy module listed.

Pure move: the classes, their statuses and their messages are unchanged.
Workspace MCP servers, workspace web search and the sandbox code-execution
policy are all the tools domain, so their seven errors leave the tenancy
module for their own (#1193).

Pure move: the classes, their statuses and their messages are unchanged.
…ptions.py

The twenty errors of the two organization guardrail surfaces, mandates and
definitions, leave the tenancy module for their own domain's (#1193). The
route docstrings that pointed at the tenancy module for where a status comes
from now name the domain module, pricing's included.

Pure move: the classes, their statuses and their messages are unchanged.
…ns.py

Passwords, passkeys, OAuth, email verification and reset, the profile, and
deployment-wide account administration are the identity domain, so their
thirty errors leave the tenancy module for their own (#1193).

Pure move: the classes, their statuses and their messages are unchanged.
One cross-reference in `deployment_user_service` names the new module.
…s_exceptions.py

The last thirty-one errors, organizations, workspaces, members, invitations,
email-domain claims and first-boot provisioning, leave for their own domain
module, and `services/tenancy/errors.py` goes with them (#1193). The design
note it carried, that a class names its own status and one handler renders
the family, moves to the `gateway.exceptions` package docstring, which is
where every caller now reaches the family. The three remaining pointers at
the old module name the package instead.

`test_tenancy_errors_home.py` asserted that the bases the old module
re-exported were the ones in `gateway.exceptions`. With no re-export left
there is nothing for it to compare.

Pure move: the classes, their statuses and their messages are unchanged.
The errors split (#1193) gives identity, organizations, guardrails, pricing
and tools a module each, and the shared set one for the errors no single
domain owns. `tenancy/errors.py` leaves the organizations services list.

The service-module counts in "The shape today" are left as they are. Their
recorded figures do not reproduce from the tree at the commit they name, so
decrementing one for the removed module would record a guess.
@peteski22
peteski22 force-pushed the refactor/1193-errors-per-domain branch from 362519a to 6d73aae Compare September 24, 2026 15:53
@peteski22
peteski22 deployed to integration-tests September 24, 2026 15:54 — with GitHub Actions Active
@peteski22
peteski22 deployed to integration-tests September 24, 2026 15:54 — with GitHub Actions Active
@peteski22
peteski22 deployed to integration-tests September 24, 2026 15:54 — with GitHub Actions Active
@peteski22
peteski22 deployed to integration-tests September 24, 2026 15:54 — with GitHub Actions Active
@peteski22
peteski22 merged commit cc60e95 into main Sep 24, 2026
18 checks passed
@peteski22
peteski22 deleted the refactor/1193-errors-per-domain branch September 24, 2026 16:02

This branch was successfully deployed

1 active deployment
integration-tests — 6d73aae5 Deployed Sep 24, 2026 by peteski22 via test-integration (1/4) #2747
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Errors into one module per domain

2 participants