Skip to content

refactor(user-management): move byte-identical base_service to autobot_shared (#12647) - #12972

Merged
mrveiss merged 1 commit into
Dev_new_guifrom
issue-12647
Jul 29, 2026
Merged

mrveiss merged 1 commit into
Dev_new_guifrom
issue-12647

Conversation

@mrveiss

@mrveiss mrveiss commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Refs #12647, part of #12645. First step of the user_management de-fork — the one file with no drift to reconcile. The issue stays open.

Thinking Path

#12647's difficulty is semantic drift: 25 shared-but-divergent files, with real behavioural differences in user_service, rbac_middleware and the models. Its own recommendation is to sub-step, starting with models/base + base_service.

Measuring that sub-step first changed its size:

$ diff autobot-backend/.../services/base_service.py autobot-slm-backend/.../services/base_service.py
(0 diff lines — byte-identical)

$ diff .../models/base.py .../models/base.py
64 diff lines

The issue lists base_service as 6 diff lines; it is now 0. So half of sub-step (a) is a move with nothing to reconcile — no drift, no feature union to decide, no risk of losing either side's behaviour. Its imports are uuid, dataclasses, sqlalchemy and autobot_shared.logging_manager — nothing backend-specific, so it is portable as-is.

models/base.py (64 diff lines) is left alone: that one carries the declarative-base question this issue and #12645 both flag, and it is the decision I should not make unilaterally.

What Changed

Verification

$ python3 -m pytest autobot_shared/user_management/base_service_test.py -q
6 passed in 2.18s

$ python3 -m pytest autobot_shared/ -q
1137 passed in 37.28s

$ python3 -m flake8 (shared package + both shims)
(clean)

import smoke: user_management.services.{base_service,user_service,organization_service}  3/3

The tests assert both shims resolve to the same class objects, not merely equivalent ones:

assert module.BaseService is BaseService
assert a.BaseService is b.BaseService

That identity check is the point. Two same-named classes are not interchangeable — the trap #12913 fixed for CircuitState and #12932 records for the memory graph — so a "de-fork" that left two equivalent-but-distinct classes would reintroduce exactly the defect this umbrella exists to remove.

Why the issue stays open

This is 1 of 25 divergent files, deliberately the easiest. What remains is the actual work: models/base.py (64 lines, gated on the declarative-base decision), rbac_middleware.py (331), database.py (440), config.py (241), user_service.py (111), and the rest — each needing drift reconciled under the issue's "union of features, zero loss" contract.

The value here is establishing the target package and the shim pattern, so subsequent moves are mechanical rather than architectural.

Model Used

claude-opus-5

…t_shared (#12647)

First step of #12645's user_management de-fork. The package is forked across
both backends with 25 shared-but-divergent files, so the risk is reconciling
semantic drift -- except base_service.py, which is byte-identical in both and
imports nothing backend-specific (uuid, dataclasses, sqlalchemy, and
autobot_shared.logging_manager). That makes it the one move with no drift to
reconcile.

Note the issue lists base_service as 6 diff lines; it is now 0, so this step is
smaller and safer than when #12647 was written.

Creates autobot_shared/user_management/ as the consolidation target, and leaves
a re-export shim in each backend so all 12 existing importers keep working --
the fork goes, not the callers. Same pattern as auth_rbac re-exporting
is_admin_role (#12948).

Tests assert both shims resolve to the SAME class objects rather than merely
equivalent ones: two same-named classes are not interchangeable, which is the
trap #12913 fixed for CircuitState and #12932 records for the memory graph.

Verified: 6 new tests pass, 1137 passed across autobot_shared, the three
user_management service modules still import, flake8 clean.
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

@mrveiss
mrveiss merged commit 2e5479b into Dev_new_gui Jul 29, 2026
40 of 41 checks passed
@mrveiss
mrveiss deleted the issue-12647 branch July 29, 2026 17:03
mrveiss added a commit that referenced this pull request Jul 31, 2026
) (#13126)

* refactor(user-management): design new canonical declarative base (#12647)

Resolve the models/base.py fork per the owner's 2026-07-31 decision on
#12645/#12647: a new canonical base that preserves both sides' properties
deliberately, not an adoption of either fork. Backend's AsyncAttrs and
eager_defaults (#4300/#11684) are kept; SLM's postgresql.UUID column typing
is kept (verified identical compiled DDL to the generic Uuid type on both
Postgres and SQLite, so no migration is implied by the type itself);
timestamps stay baked into Base unconditionally, matching backend's
existing, #10636-hardened design (backend's Base also backs ~40
non-user_management models, so making timestamps opt-in would silently
drop created_at/updated_at from all of them).

Canonical implementation lives in autobot_shared/user_management/models/base.py;
both backends keep re-export shims so existing importers are unaffected,
following the base_service (#12972) / schemas.user (#13007) precedent.

* refactor(user-management): converge backend model UUID typing to canonical (#12647)

Replace sqlalchemy.types.Uuid with sqlalchemy.dialects.postgresql.UUID in the
8 gated model files' explicit column declarations, matching the new canonical
base's type_annotation_map (SLM's existing typing) and eliminating the last
mechanical divergence in these files. Also add the OKTA provider type SLM
already has (feature-parity union, #12647 contract) to sso.py's
SSOProviderType and is_enterprise() check.

Verified: postgresql.UUID is a subclass of the generic Uuid in SQLAlchemy
2.0 (Emulated/NativeForEmulated) and compiles to identical DDL on Postgres
and SQLite — no migration or behavior change from the type swap itself.

Remaining diff in these files is now only legitimate per-side content:
user.py's activity-tracking relationships and organization.py's LLC/PM-sync
extension columns are backend-only features, left untouched.

* refactor(user-management): converge SLM models onto canonical base (#12647)

Drop the now-redundant TimestampMixin from class bases across the 7 model
files that declared it — the new canonical Base bakes created_at/updated_at
in unconditionally, so TimestampMixin is a no-op alias; combining it with
Base maps the same columns once, not twice (no metaclass conflict, #4300).
Adopt now_utc() (autobot_shared.time_utils) in place of inline
datetime.now(timezone.utc) calls, matching the backend's existing pattern.

Also: fix sso.py's is_enterprise() (SLM added SSOProviderType.OKTA but never
added it to the enterprise check) and adopt the shared
CategoryDefaults.ROLE_USER constant in place of the literal "user" default,
matching backend's existing SSOT usage.

After this, role.py, team.py, api_key.py, mfa.py, sso.py, and audit.py are
identical to their backend counterparts except cosmetic typing style
(Optional[X] vs X | None) and, for sso.py, SLM's accurate SystemSecret
documentation — both legitimate, left as-is. user.py and organization.py
retain backend's activity-tracking relationships and LLC/PM-sync columns
respectively, which are real per-side features, not fork residue.

* refactor(user-management): add SLM timestamp migration for role_permissions/audit_logs (#12647)

The new canonical base bakes created_at/updated_at into every model
unconditionally (backend's existing design). SLM's RolePermission and
AuditLog models never opted into TimestampMixin and have no matching
columns in the live SLM database, so they now expect columns that don't
exist yet. Add an idempotent, guard-checked migration (mirrors the
backend's own #10636 fix for the identical drift) and wire it into the
runner's migration list.

Forward-only, additive, no data loss: ADD COLUMN ... DEFAULT NOW() backfills
existing rows via the server-side default, matching #10636's proven pattern.

* fix(redis): guard RedisConnectionManager.__init__ against re-entrant recursion (#13128)

migration-gate's test_baseline_states.py (and every test in
autobot-backend/tests/migrations/) failed with a RecursionError surfacing
as "Failed to get sync Redis client for database 'main': maximum recursion
depth exceeded" once import models started reaching this code path earlier
in the import graph (via the new models/base.py shim pulling in
base_service.py's module-level get_logger() call).

Root cause (confirmed via a captured traceback from a monkeypatched
get_sync_client, no live app/DB/Redis involved): constructing
RedisConnectionManager for the first time resolves its own Redis host via
NetworkConstants.REDIS_VM_IP, which falls through to ConfigRegistry.get()'s
Redis-backed cache tier, which constructs a Redis client, which constructs
this same singleton again. __new__ correctly returns the same
not-yet-initialized instance, but Python still calls __init__ on it again —
and the old _initialized guard was set only at the very end, so the
re-entrant call saw no guard and reran every init step from scratch,
recursing without bound.

Mark _initializing = True before running any init step; a re-entrant call
now returns immediately, letting its caller (ConfigRegistry._get_redis(),
already wrapped in a broad except) degrade gracefully to the
env/registry-defaults tier — one clean failure instead of an unbounded
recursive stack. Filed as #13128 (pre-existing, exposed rather than
introduced by this PR's import-order change).

* fix(user-management): make autobot_shared.user_management re-exports lazy (#12647)

migration-gate's deliberately thin dependency set (no email_validator, same
defect class as the jsonschema coupling PR #13053 fixed) broke because
importing autobot_shared.user_management.models.base (SQLAlchemy +
time_utils only) forced Python to first execute the PARENT package's
__init__.py in full — which eagerly imported schemas.user (pre-existing,
PR #13007), whose EmailStr fields build a pydantic CoreSchema requiring
email_validator at class-definition time. That pulled email_validator into
`import models`'s chain for the first time via the models/base.py shim.

Confirmed statically (blocked `import email_validator` in a scratch script,
no app/service execution): the shim import failed against the old eager
__init__.py with the exact ImportError from CI, and succeeds against this
fix.

Convert the package's re-exports to lazy attribute resolution (PEP 562
module __getattr__), mirroring the pattern autobot_shared/__init__.py
itself already uses. Each submodule's dependency footprint now stays
scoped to callers that actually touch it — importing one mover no longer
drags in the others. Identity (`pkg.Base is Base`, etc.) is preserved since
__getattr__ resolves to the same module objects.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
mrveiss added a commit that referenced this pull request Aug 1, 2026
…t_shared (#12647) (#13164)

* refactor(user-management): move byte-identical team_service to autobot_shared (#12647)

team_service.py was byte-identical across autobot-backend and
autobot-slm-backend — 701 duplicated lines, so a fix landed in one service
or the other but never both by accident.

It is the first user_management *service* that could move: its entire
runtime dependency set already lives in autobot_shared — Team/TeamMembership
and the audit vocabulary (#13130), BaseService/TenantContext (#12972). Its
only mentions of User and Organization are in docstrings and error strings,
never as classes, so it does not need the concrete per-backend models.

Placed flat as autobot_shared/user_management/team_service.py, matching the
existing base_service.py rather than inventing a second services/ location.

Both backends keep a re-export shim so all existing importers, including
each services/__init__.py, are untouched.

organization_service.py is also byte-identical but deliberately not moved:
it imports the concrete User and Organization at runtime, and those stay
backend-local by design.

* docs(session): handoff for issue-12647 team_service slice (#12647)

* fix(user-management): correct two latent type errors exposed by strict mypy (#12647)

team_service.py moved into autobot_shared/, which CI type-checks strictly; the
backend copy never was. Two latent errors surfaced:

- _apply_team_name_update declared name: str but update_team passes
  str | None. The body already guards with 'if name and ...', so this was a
  wrong annotation rather than a live crash — widened to str | None.
- changes needed an explicit annotation; typed as dict[str, dict[str, Any]]
  to match the {'old': ..., 'new': ...} audit payload it carries.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

1 participant