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 it | Backend services at build time. Nothing calls these over the network. |
| Runtime | None of their own — they run inside whichever service imports them. No Deployment, no Service, no image. |
| Database | None. |
| Redis | None. |
| NATS streams | None. |
| External APIs | Indirect 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 points | 0 HTTP routes · 0 NATS consumers · 0 background jobs · 4 importable packages |
| How they get installed | Each 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
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
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 bySENTRY_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 theX-Tenant-ID/X-User-IDheaders.
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 bylibs/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
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/*.jsonplusvalidate_block_dataand thenormalize_*helpers (text, speak, speak-choice, select-word, divider).
Duration maths:estimate_screen_seconds,lesson_minutes,resolve_lesson_minutesand 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:8mentionsoper_toolsin 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 inmicro-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
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-basedTenantContext/current_tenant_id/set_current_tenant_idfor 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 byauthoring-service-v2/tests/unit/test_tenancy.py. Not dead — but it is the thinnest dependency inlibs/, and the import site carries asys.pathfallback 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-tenancyshows onlyoper_tenancy/andpyproject.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
| Subject | Stream | Durable | Published by | Feature | Verdict |
|---|---|---|---|---|---|
| None. | |||||
Publishes
| Subject | Consumed by | Feature | Verdict |
|---|---|---|---|
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
| Job | Schedule | What it does | Verdict |
|---|---|---|---|
| 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 point | Kind | Verdict | Evidence |
|---|---|---|---|
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
libs/auth/README.md,libs/auth/README_SERVICE_AUTH_KIT.md,libs/auth/SERVICE_AUTH_MATRIX.md,libs/auth/SERVICE_AUTH_FUNCTIONS.md.libs/observability/README.md;libs/oper-tools/README.md.- Package sources read directly:
libs/auth/oper_auth/__init__.py,libs/observability/observability/__init__.pyandinit.py,libs/oper-tools/oper_tools/__init__.py,libs/oper-tenancy/oper_tenancy/__init__.py, and all fivepyproject.tomlfiles. - Importer census: repo-wide search for
oper_auth,observability,oper_toolsandoper_tenancyacross serviceapp/trees, cross-checked against theCOPY libs/…lines in every serviceDockerfile. ARCHITECTURE.md§5 "Dead weight" — thelibs/ToDelete/andlibs/oper-tools/build/entries this page verifies and corrects.