fix(seam): scope the enterprise ImportError arm to the top-level import (#1653) - #1739
Conversation
…l" (ent#196)
The enterprise load block caught bare ImportError and printed
Trinity Enterprise submodule not present — OSS-only build
(this is normal; enterprise modules are an optional private submodule)
That is right for an OSS clone. But ImportError also covers a MOUNTED submodule
whose module failed to import — e.g. importing a name from an OSS table that
hasn't landed yet. Observed live: the submodule was present and correctly
mounted, a module failed, and the operator was told "this is normal" while a
paid feature was silently absent. Diagnosing it took a hand-run
register_enterprise() to see the real traceback.
Narrow the benign branch to its actual signature — a ModuleNotFoundError naming
the `enterprise` package itself. A plain ImportError (missing NAME) and a
ModuleNotFoundError for anything else (a missing third-party dep) now fall
through to the existing loud handler, which was already correct and is only
factored out into _report_enterprise_failure so both paths share it.
test_847_entitlement_seam pinned the literal
`from .sso import register as register_sso` in the enterprise __init__.py. The
paired enterprise change registers each module through an isolating helper, so
that pair no longer appears; the pin moves to the intent (the submodule
registers sso) rather than the import style, and still accepts the old form.
Related to Abilityai/trinity-enterprise#196
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ent#196) The registration seam is a CLAIM that a feature is present and served. A module that claims its feature_id and then fails partway through mounting leaves that claim standing with nothing behind it: advertised by GET /api/settings/feature-flags, UI rendered by the OSS bundle, every request 404s. Give the registering side a way to roll its own claim back so a half-registered module is simply absent — the honest state. Idempotent; withdrawing an unregistered id is a no-op. Used by the paired enterprise change, which snapshots the registry around each module's register() and withdraws whatever it claimed before failing. That lives on the private side because only it knows the claim boundary; this is the edition-agnostic primitive. Related to Abilityai/trinity-enterprise#196 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extensive testing + one additionThe OSS-only path, verified on a genuinely submodule-less buildThe backend image ships without So the narrowed branch still reports a real OSS clone correctly — the change doesn't regress the case the message exists for. The loud path was observed live in the opposite direction earlier: with the submodule mounted and Alembic unable to resolve a revision, boot printed Added:
|
…rt (#1653) main.py wrapped the enterprise import AND register_enterprise(app) in one try whose first arm was `except ImportError`. ModuleNotFoundError subclasses ImportError, so a real bug raised inside module registration — enterprise modules lazily import OSS seams in their register() — printed Trinity Enterprise submodule not present — OSS-only build (this is normal…) On a mounted install that is actively false: the diagnostic traceback never ran, boot did not crash, and the failing module's routes could stay mounted while its entitlement was never registered, 403-ing forever with nothing in the logs connecting the two. Reported by @vybe on #1653. The fix has two parts and both are needed. My earlier pass here narrowed the arm to ModuleNotFoundError + a check that the missing module IS the enterprise package. That alone is not sufficient: a module doing `from .missing_sibling import x` inside register() raises ModuleNotFoundError whose name starts with `enterprise`, so the name check calls it benign — the very case the issue is about. So adopt #1653's structural suggestion too: the ImportError arms now guard ONLY the top-level import, and register_enterprise(app) moves to the else arm under `except Exception`, which catches everything it can raise. The name check is kept for the top-level import, where the structure cannot help: if enterprise/backend/__init__.py needs a third-party package that isn't installed, that is a real failure wearing the same exception type as an absent submodule. Verified live, all four branches: absent submodule (real OSS tree, no enterprise dir) -> calm line, boots clean missing third-party dep in the top-level import -> diagnostic `from .gone_module import x` inside register() -> diagnostic (ModuleNotFoundError name='enterprise.backend.gone_module' — the trap case) the live product_events failure -> diagnostic, real cause CI string assertions unchanged and pinned by a test: build-without-submodule.yml greps for "Trinity Enterprise submodule not present". Related to #1653, Abilityai/trinity-enterprise#196 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This PR now also fixes #1653 — and #1653's approach found a hole in mine#1653 (credit @vybe) describes exactly the bug this PR was opened for; I hit it independently while testing trinity-enterprise#196. Reading it, its suggested fix is better than my original one in a case I had wrong, so I've adopted it. The hole in the name-check-only approachMy first pass kept one
A module doing Both parts are needed
Verified live — all four branchesThat last one is the bug in the wild: this instance currently has trinity-enterprise#184's telemetry module importing an OSS table whose OSS half (#1721) hasn't merged. Before: "submodule not present — this is normal". After: Acceptance criteria (#1653)
Tests
Also pins the CI string contract — Related tests: 87 passed, 3 skipped. |
|
Resolve by running |
vybe
left a comment
There was a problem hiding this comment.
Validated via /validate-pr: OSS half of the ent#196 seam pair (unregister_module + honest ImportError narrowing), named regression tests, security clean. Enterprise half (ent#197) merged.
Fixes #1653.
Related to Abilityai/trinity-enterprise#196 (OSS half; the enterprise half — per-module registration isolation — is Abilityai/trinity-enterprise#197).
The bug
The enterprise load block caught bare
ImportErrorand printed:Correct for an OSS clone. But
ImportErroralso covers a mounted submodule whose module failed to import — e.g. importing a name from an OSS table that hasn't landed yet.Observed live: the submodule was present and correctly mounted, a module failed, and the operator was told "this is normal" while a paid feature was silently absent. Diagnosing it required hand-running
register_enterprise()in the container to see the real traceback.The fix
Narrow the benign branch to its actual signature — a
ModuleNotFoundErrorwhose missing module is theenterprisepackage:ModuleNotFoundError(name="enterprise.backend")ImportError(not aModuleNotFoundError)ModuleNotFoundError(name="pyotp")ExceptionThe loud handler was already correct and is only factored into
_report_enterprise_failure()so both paths share it. No behavior change for a genuine OSS build.Test-pin update
tests/unit/test_847_entitlement_seam.pystatically pinned the literalfrom .sso import register as register_sso+register_sso(app)in the enterprise__init__.py. The paired enterprise PR registers each module through an isolating helper, so that exact pair no longer exists.The pin moves to the intent — "the submodule registers
sso" — and still accepts the pre-ent#196 form, so this cross-repo static check doesn't break on a behavior-preserving refactor in the other repo.Verification
New
tests/unit/test_ent196_enterprise_load_reporting.py(4 passed, 1 skipped — the skip is the "no submodule" case, which can't be exercised in a checkout that has one). Includes a static guard that fails if a bareexcept ImportErrorever comes back in front of the benign message.Full OSS unit suite: 4667 passed, 16 skipped, 5 failed. All 5 failures reproduce on a clean
origin/devworktree and are unrelated:test_admin_email_login—AttributeError: module 'bcrypt' has no attribute '__about__'(local bcrypt/passlib version mismatch)test_1474_read_boundary_z::test_schedules_summary_last_run_at_normalizedtest_1472_schedule_validation::test_accepts_valid[0 4 * * *-Europe/Kiev]Enterprise-docs guard run locally against
docs/,CLAUDE.md,main.py,entitlement_service.py— clean (the new comment describes the generic seam and names no module).Verified live: with the enterprise submodule mounted and one module genuinely broken, the log now reads
Trinity Enterprise registration completed with 1 FAILED module(s): …instead of the reassuring line.🤖 Generated with Claude Code