libs/ — the four shared packages

What every service gets for free, and who actually imports it · ← platform hub · no scan contract — these are libraries, not services

1. What it is

libs/ holds the four Python packages every Oper service shares, so that the rules that must be identical everywhere — who is allowed to do what, how errors reach Sentry, what a lesson block is allowed to contain — live in one place instead of being re-implemented per service. They are not deployed; they are copied into each service image at build time and installed there.

Who uses itBackend services at build time. Nothing calls these over the network.
RuntimeNone of their own — they run inside whichever service imports them. No Deployment, no Service, no image.
DatabaseNone.
RedisNone.
NATS streamsNone.
External APIsIndirect only: oper-auth can call users-auth's token-validation endpoint and fetch JWKS on behalf of its host service; observability ships events to Sentry.
Entry points0 HTTP routes · 0 NATS consumers · 0 background jobs · 4 importable packages
How they get installedEach service Dockerfile copies the directories it needs into /tmp and runs pip install --no-cache-dir --no-deps on them. They are deliberately absent from requirements.txt — e.g. authoring-service-v2/Dockerfile:12-17, feed-service/Dockerfile:14-15.

2. Feature map

flowchart LR
  AUTH["libs/auth · oper_auth"] --> S1["7 services: users-auth, feed, assignment, micro-learning, asset-manager, notification-worker, authoring-v2"]
  OBS["libs/observability · observability"] --> S2["12 services and workers"]
  TOOLS["libs/oper-tools · oper_tools"] --> S3["authoring-v2, content-worker, micro-learning"]
  TEN["libs/oper-tenancy · oper_tenancy"] --> S4["authoring-v2 only"]
  DEL["libs/ToDelete"] --> NONE["nobody"]
  BUILD["libs/oper-tools/build/"] --> NONE

3. The packages

Internal (other services)

libs/auth — oper-auth 0.1.5 live

internal

The single answer to "is this caller allowed to do this?". It verifies a user's or admin's token, says which permissions that token carries, checks the shared internal key that services use to talk to each other, and refuses a request whose token belongs to a different company than the one named in the header. Every service that enforces access does it through this package, so a permission rule changes in one file rather than nine.

What it gives you
HybridJWTValidator — validate locally against JWKS, or via users-auth's /v1/internal/validate-token, or prefer JWKS and fall back on a JWKS outage.
Scope constants and the admin-role → scope map (FEED_WRITE, CONTENT_PUBLISH, BILLING_WRITE, ROLE_SCOPES, …) so role names mean the same thing in every service.
Tenant and header policy: require_header, enforce_tenant_match, require_user_type, require_any_scope, parse_uuid_field.
Internal-key auth: assert_oper_internal_key (constant-time), authorize_internal_service_request, authenticate_bearer_or_oper_key, build_require_admin_dependency.
A ready-made FastAPI wiring layer: build_auth / AuthKit / AuthContext (oper_auth/service/).
Who imports it
7 services: users-auth-service/app/core/scopes.py · feed-service/app/core/security.py · assignment-service/app/core/security.py · micro-learning-service-v2/app/core/security.py · asset-manager-service/app/core/auth.py · notification-worker/app/services/auth_service.py · authoring-service-v2/app/tools/scopes.py
Related
libs/auth/SERVICE_AUTH_MATRIX.md (per-service inbound/internal/outbound auth model) · libs/auth/SERVICE_AUTH_FUNCTIONS.md (what each service takes from the library vs keeps local) · libs/auth/README_SERVICE_AUTH_KIT.md
Evidence
libs/auth/oper_auth/__init__.py:1-49 (public surface) · libs/auth/pyproject.toml:6-7 · tests at libs/auth/tests/ (scopes, service auth, JWKS rotation)

libs/observability — oper-observability 0.1.0 live

internal

One line of setup gives a service crash reporting with the context an on-call engineer actually needs: which service, which company, which user, and — for the NATS workers — which event was being processed when it blew up. Without it, an exception in a worker is an anonymous stack trace with no way to find the affected customer.

What it gives you
init_observability(service_name=…) — Sentry bootstrap driven by SENTRY_DSN, ENV, RELEASE, SENTRY_TRACES_SAMPLE_RATE; the FastAPI integration switches itself on when FastAPI is present.
install_request_context_middleware(app, …) — tags every HTTP event with the X-Tenant-ID / X-User-ID headers.
worker_event_scope / bind_worker_event — the same tagging for a NATS message handler.
Helpers: capture_exception, capture_message, event_scope, request_scope, set_identity_tags, current_service_name, is_initialized.
It also filters out uvicorn's lifespan-cancellation traceback, which is normal pod-teardown noise rather than an application error.
Who imports it
12 services and workers: asset-manager · assignment · content-worker · image-worker · authoring-v2 · feed · micro-learning v2 (incl. the TTS worker) · notification-worker · rag-context-worker · stt-service · suggest-pipeline-worker · users-auth (incl. the onboarding-enrichment worker). Most import it defensively — try: from observability import … except Exception: with no-op stubs — so a service still boots if the wheel is missing (e.g. feed-service/app/main.py:17-24, stt-service/app/main.py:38-42).
Related
libs/observability/README.md · duplicated by libs/ToDelete/, see §8
Evidence
libs/observability/observability/__init__.py:1-27 · noise filter at libs/observability/observability/init.py:60-79 · tests at libs/observability/tests/test_init.py

libs/oper-tools — oper-tools 0.3.0 live

internal

The shared definition of "what a lesson is made of". Authoring, the content worker and the learner-facing service all agree on the block types, on what a valid block payload looks like, on the list of actions the authoring AI may call, and on how long a lesson takes to complete. Without one copy, the chat assistant's "regenerate this block" and the UI's version would validate differently and estimated durations would disagree between the editor and the app.

What it gives you
catalog.json — every domain action authoring exposes as a tool, with input and output JSON Schema, sync/async classification, idempotency policy, required capabilities, cost class and the NATS subject for async tools.
block_schemas/*.json plus validate_block_data and the normalize_* helpers (text, speak, speak-choice, select-word, divider).
Duration maths: estimate_screen_seconds, lesson_minutes, resolve_lesson_minutes and the shared bounds.
Typed models: ToolDefinition, ToolCallRequest, ToolCallResult, BlockType, CostClass.
Who imports it
3 services, matching the three Dockerfiles that install it: authoring-service-v2 (tool dispatcher, publish, drafts, tree ops, tool routes) · authoring-content-worker/app/pipeline/block_writer.py · micro-learning-service-v2/app/services/lesson_duration_service.py. (users-auth-service/app/core/scopes.py:8 mentions oper_tools in a comment only — it is not an importer.)
Related
libs/oper-tools/README.md — how to add a block type or a tool · block envelope spec in micro-learning-service-v2/spec/lesson_blocks.md
Evidence
libs/oper-tools/oper_tools/__init__.py:1-37 · Dockerfiles: authoring-service-v2:14, authoring-content-worker:13, micro-learning-service-v2:21 · tests at libs/oper-tools/tests/ (catalog, block validation, duration, text normalisation)

libs/oper-tenancy — oper-tenancy 0.1.0 live

internal

A deliberately tiny guardrail against the worst bug the platform can ship: a database query that forgets which company it is for and returns another customer's data. Decorating a repository method makes it fail loudly at call time if no company id was passed, so the mistake surfaces in development instead of as a cross-tenant leak in production.

What it gives you
@requires_tenant_id (works on sync and async methods, positional or keyword), MissingTenantId, and a contextvar-based TenantContext / current_tenant_id / set_current_tenant_id for jobs and tests.
Who imports it
One production importer: authoring-service-v2/app/repositories/module_repository.py:17, which decorates four repository methods (:63, :74, :83, :92). Also exercised by authoring-service-v2/tests/unit/test_tenancy.py. Not dead — but it is the thinnest dependency in libs/, and the import site carries a sys.path fallback for the case where the wheel is not installed (:18-23).
Related
No README and no tests of its own — the only coverage lives in the consuming service. That is the gap worth closing before a second service adopts it.
Evidence
libs/oper-tenancy/oper_tenancy/__init__.py:1-143 · installed by authoring-service-v2/Dockerfile:15 and :17 · ls libs/oper-tenancy shows only oper_tenancy/ and pyproject.toml

Admin

None — no package here is reachable by an end user or an admin.

Employee (mobile)

None.

Background

None. observability installs a Sentry before_send hook and oper-auth's JWKS validator caches keys with a TTL, but neither starts a task, a thread or a timer of its own.

4. API reference

No HTTP surface — these are libraries. Nothing in libs/ defines a route, mounts a router, binds a port or appears in any Kubernetes manifest. The only route-shaped code is oper_auth's FastAPI dependencies (oper_auth/service/dependencies.py, oper_auth/service/fastapi.py) and observability's request middleware (observability/fastapi.py) — both of which attach to a host service's routes and own none themselves. The public surface of each package is listed in §3.

5. Async contracts

Consumes

SubjectStreamDurablePublished byFeatureVerdict
None.

Publishes

SubjectConsumed byFeatureVerdict
None. oper_tools's catalog.json records the NATS subject of each async tool, but the library never connects to NATS or publishes anything — the host service does.

Background jobs

JobScheduleWhat it doesVerdict
None.

6. Data it owns

No database, no tables, no Redis keys, no bucket. The packages are stateless and own no runtime data.

The one thing they do own is shipped, versioned reference data inside the wheel: oper_tools/catalog.json (the tool catalog) and oper_tools/block_schemas/*.json (the per-block-type payload schemas). Changing either is a library version bump that both consuming services then pick up (libs/oper-tools/README.md:19-27) — which is exactly why the stale copy under build/lib/ matters, see §8.

7. Dependencies

flowchart LR
  UA["users-auth-service"] --> A["oper_auth"]
  FEED["feed-service"] --> A
  ASGN["assignment-service"] --> A
  ML["micro-learning-v2"] --> A
  AM["asset-manager"] --> A
  NW["notification-worker"] --> A
  ASV2["authoring-service-v2"] --> A
  ASV2 --> T["oper_tools"]
  CW["content-worker"] --> T
  ML --> T
  ASV2 --> TEN["oper_tenancy"]
  ALL["all 12 services and workers"] --> O["observability"]
  A --> UAEP["users-auth validate-token + JWKS"]
  O --> SENTRY["Sentry"]

Outbound edges are made by the host service's process, not by an independent workload: oper_auth's hybrid validator calls users-auth's /v1/internal/validate-token (sending X-Oper-Key) and fetches JWKS; observability ships to Sentry. Inbound: nothing — these packages are imported, never called.

8. Dead-code verdicts

Every directory under libs/ with no importer. Deleting is a separate decision — see the hub roll-up.

Entry pointKindVerdictEvidence
libs/ToDelete/ Python package dead No importer: a repo-wide search for ToDelete across *.py, Dockerfile*, *.toml and *.txt matches nothing outside vendored site-packages noise; no service Dockerfile copies it.
Unreachable in principle under its own name: it declares name = "oper-observability" — the same distribution name as libs/observability (libs/ToDelete/pyproject.toml:6 vs libs/observability/pyproject.toml:6) and the same top-level module observability/. It can therefore never be imported as itself; the only thing it can do is shadow the real package if it were ever installed after it.
Correction to ARCHITECTURE.md §5: it is described there as "byte-identical". It is no longer: libs/ToDelete/observability/init.py is 69 lines against the real package's 90, and lacks the Sentry before_send filter that drops uvicorn's lifespan-cancellation noise. So the shadowing risk is worse than recorded — a shadowed install would silently regress error filtering, not merely duplicate it.
libs/oper-tools/build/ Packaging artifact dead No importer: a tracked setuptools output directory (build/lib/oper_tools/) that duplicates the source tree; nothing imports from build/ and no Dockerfile references it. ARCHITECTURE.md §5 recommends adding build/ to .gitignore.
Already diverged, which is the real hazard: build/lib/oper_tools/catalog.json is 34,306 bytes against the source's 45,362 — it is a stale snapshot of the tool catalog. Anyone reading or grepping the repo for tool definitions can land on the wrong copy.
libs/oper-tenancy/ Python package live Recorded here because it is the obvious deletion candidate and it is not one. Exactly one production importer — authoring-service-v2/app/repositories/module_repository.py:17, four decorated methods at :63, :74, :83, :92 — installed by authoring-service-v2/Dockerfile:15,17. Removing it would silently drop a cross-tenant guardrail.
*.egg-info/ in libs/auth, libs/observability, libs/oper-tools Packaging artifact dead Editable-install metadata left in working trees. No importer and not shipped: git ls-files libs | grep egg-info returns zero files, so none of it is tracked — it is local residue that never reaches an image (the Dockerfiles pip install the package directories, which regenerate their own metadata). Contrast libs/oper-tools/build/, which is tracked.

9. Sources