---
title: "Platform Updates — June 2026"
slug: "platform-update-2026-06-30"
date: "2026-06-30"
tag: "Application"
summary: "74 production image releases (1.25.1 → 1.61.1): agent-driven edX course creation, streaming text-to-speech, first-class privacy-mode chat, the agentAI→OS and Skills→LMS renames, principal-agnostic RBAC, and optional AWS WAF in the infra CLI."
author: "ibl.ai Engineering"
---

Production image releases `1.25.1` → `1.61.1` (74 releases, June 2026).

| service | image / pin | version span |
|---|---|---|
| dm | `iblai-dm-pro` | `4.267.1` → `4.294.1-ai` |
| os | `iblai-os-spa` | `0.70.1` → `0.88.2` |
| lms | `iblai-lms-spa` | `0.36.0` → `0.43.2` |
| edx | `iblai-edx-pro` | `sumac.2.32.0` → `sumac.2.36.0` |
| cli | `iblai/infra-cli` | `1.11.0` → `1.12.0` |

---

## `iblai-dm-pro` — `4.267.1` → `4.294.1-ai`

**Text-to-speech grew up.** Chat-message TTS now streams audio from the provider so playback starts before the whole clip is encoded, and messages over 4000 characters are split on sentence boundaries and re-encoded through a single `lameenc` MP3 encoder into one gapless stream (new `lameenc==1.8.2` dependency). Agents gained a `voice_instructions` field that controls the reading style — passed to OpenAI as the speech `instructions` (switching synthesis to `gpt-4o-mini-tts`) and to Google as the synthesis `prompt`. Google TTS moved to the Gemini TTS model (`gemini-3.1-flash-tts-preview`), auto-detecting language from the text via `langdetect`. Message text is HTML-stripped before synthesis via `strip_html_tags`, which preserves accessibility content (`alt`, `aria-label`, `title`) so it is spoken rather than dropped, and drops `aria-hidden="true"` elements so screen-reader-hidden content is never read aloud.

**Emoji and symbol handling for spoken output was iterated to a stable rule.** An initial pass rewrote decorative emoji and symbols into their spoken names before synthesis (new `emoji==2.15.0` dependency); a follow-up narrowed this so only emoji that belong to a math equation are kept and spoken (inside delimited LaTeX/markdown maths such as `$...$`, `\(...\)`, or next to a math operator), while every other decorative emoji is stripped. The `unicodedata` symbol-name fallback was removed so math symbols (→, ∑, ≈) pass through untouched instead of being mangled into verbose Unicode names. Authorization for TTS is a plain session-ownership check — only the session's student may synthesize its messages (`403` otherwise).

**Privacy mode became a first-class, fully-featured chat state.** The PII privacy router (`Mentor.privacy_action`) gained an `allow` option alongside `redact`/`mask`/`block`: detected PII is passed to the LLM unchanged but still written to `ModerationLog` for audit (migration `0327_alter_mentor_privacy_action`; default stays `redact`). More significantly, DISABLED (privacy-mode) sessions — which previously lost all context between turns because history writes short-circuited — now retain in-session context via an ephemeral, Redis-only conversation history (24h TTL, size-bounded, refreshed each turn, never persisted to Postgres). `IBLChatMessageHistory` writes append to the cache and all four read paths read from it; `get_guided_prompts` falls back to it when there are no history rows. Because session-driven privacy severs `Session.student` (`student=None`), a series of fixes restored the authenticated owner across the affected surfaces: chat file uploads and the guided-prompts endpoint, artifact reads (via a short-lived username-keyed ephemeral grant cache covering `retrieve`/`versions`/`version_detail` only), TTS for student-less messages (addressed by `generation_id`, with the `/tts/` route converter widened from `<int>` to `<str>`), and voice calls (the call-credentials endpoint now issues credentials for student-less privacy sessions and the voice path skips the per-student cloned-voice lookup). A separate fix made chat privacy mode work with RBAC-enabled non-anonymous agents by falling back to `flow.username` for policy evaluation.

**Per-agent chat-history and RAG tuning replaced hardcoded limits.** Three new nullable `Mentor` columns — `chat_history_max_tokens`, `chat_history_max_messages`, and `rag_retrieval_k` — make retention and main-RAG `k` configurable per agent (migration `0332_mentor_chat_and_rag_limits`), each coalescing NULL to a module default (15000 tokens / 20 messages / k=4) so existing rows are unchanged; the default token cap was raised from the prior hardcoded 2000 to 15000. `IBLChatMessageHistory` now enforces a dual cap (token budget AND message count) across all four read entry points, resolved per-agent via `_resolve_history_caps`, and the main-RAG retriever folds the resolved `k` into `search_kwargs` via `_get_retriever_search_kwargs`. Separately, chat-time `payload.metadata` (product group, state, level, etc.) is now folded into the RAG query string sent to the vector store so client-supplied metadata influences retrieval.

**The page-content HTML-stripping behavior converged on a per-agent toggle.** Whether the current web page's raw HTML (`payload.page_content`) reaches the LLM went through several revisions — stop stripping (so inline questions like "what does the Submit button do?" work), then a revert (raw HTML was bloating prompts past the context window), then a tenant-wide `STRIP_PAGE_CONTENT_HTML` setting with an admin endpoint — before settling on a per-agent `Mentor.strip_page_content_html` column (migration `0326`, default `False`). The tenant-wide setting, its `PageContentConfigView` admin endpoint, and serializer were removed; `should_strip_page_content_html` now resolves the toggle from the session's agent. `strip_html` was also hardened to preserve and explicitly surface accessibility names (`[alt: ...]`, `[aria-label: ...]`, `[title: ...]`) while decomposing `<script>`/`<style>` blocks entirely.

**Agent-driven course creation shipped end to end.** A `CourseCreationToolkit` (registered as `"Course Creation"` in `TOOL_MAPPING`, replacing the old `course_stepwise` toolkit) lets an agent create a draft course, generate and push the outline, schedule background content drafting, and publish to edX. A component-type system (`tutorial` / `assessment` / `essay assessment` `ComponentType` enum) dispatches per-type drafters and encodes the type in the edX unit name via reserved prefixes. Graded assessment units provision a per-unit grading agent (`Mentor` + `MentorGraderConfiguration` + `GraderCriterion` rubric) and wire an `ibl_mentor_xblock` into the unit, with a structured multiple-choice schema (`AssessmentChoice`/`AssessmentQuestion`/`AssessmentComponentOutput`) enforcing choice bounds and exactly-one-correct. The `create_course` Celery task atomically claims its row to prevent duplicate runs, bounds retries via an `attempts` counter (max 3), and runs on the AI-agent queue with 4h/4.5h time limits; idempotency guards skip drafting when edX already has the outline/unit/component. Course-creation responses now surface the real edX `course_id` (`course-v1:org+number+run`) instead of the database PK, and a new `embed_show_canvas` boolean controls whether a display canvas is shown when an agent is embedded on another page.

**The Claw / OpenClaw gateway expanded into a full management surface.** `OpenClawClient` gained cron, MCP-server, and config-management RPCs, unified under a new `BaseClawClient` abstract parent, and exposed through REST endpoints scoped to a `ClawInstance` under `/api/ai-mentor/orgs/<org>/claw/instances/<server_pk>/`: `config/` (merge-patch of `openclaw.json` with a `__OPENCLAW_REDACTED__` secret sentinel and `baseHash` optimistic concurrency), `mcp-servers/` CRUD, `cron-jobs/` create/list/run with discriminated `schedule.kind` and `payload.kind` unions, `cron-runs/`, `cron-status/`, and `wake/` one-shot text injection. Dispatch runs the async claw client inline under `asyncio.run` + `asyncio.wait_for`, mapping outcomes to `200/201/204`, `501` for unsupported claw types (e.g. cron on IronClaw), `502` for worker errors, and `504` for upstream timeouts, with full `@extend_schema` OpenAPI annotations and a 24-request Postman collection. Later releases added an agent skills library with three-tier scoping and per-agent skill push (recording push status on `ClawMentorConfig`), a `cert_expires_at` field on `ClawInstance` for TLS near-expiry checks, and improved Claw worker health checks, routing, device pairing, skill cleanup, and DM-initiated worker upgrades.

**Agents gained a `segment` grouping.** A new `Segment` model (`name`, `description`) and a nullable `Mentor.segment` FK (migration `0328_segment_mentor_segment`) group agents by vertical, with initial segments — Legal, HealthCare, Financial Services, Government, Small Business, Enterprise, K-12, and Higher Education — seeded via migration and the `seed_mentor_metadata` command.

**Credential storage moved to encryption-at-rest.** Credential reads now treat encrypted columns as the sole source of truth: `FIELD_ENCRYPTION_KEY` is required at startup, writes update only encrypted fields, plaintext fallback paths were removed, and migration `0323_backfill_encrypted_credentials` backfills encrypted columns. Deployments must configure a stable `FIELD_ENCRYPTION_KEY` before starting `4.269.0-ai` and must not rotate it during rollout.

**RBAC was re-architected to be principal-agnostic.** `dl-core-app` 3.0.0 made `RbacEnforcer` policy-source agnostic — it now takes a `PolicySource` (e.g. `UserPolicySource(user_pk)`) instead of a bare `user_pk`, with a new `core/rbac_policy_sources.py` holding the `PolicySource` protocol, `UserPolicySource`, a deny-all `NullPolicySource`, and `policy_source_for()` dispatch. As a security consequence, non-user principals resolve to a deny-all source so a Django auth-user pk can no longer collide into another principal's policies. Four new composable global roles — `Limited Editor`, `Limited Viewer`, `Dataset Curator`, `Global Analytics Viewer` — were added (seeded via idempotent `seed_rbac_data`), along with optional `description` fields on `RbacRole`/`RbacPolicy`. A `PlatformApiKey.mode` field (`owner` default or `token_policies`) lets a key derive permissions from policies attached directly to the token; `token_policies` keys are denied by default and only accepted by views that explicitly enable them through `RbacAuthGate`.

**Consolidated tokens and server-to-server provisioning matured.** A tenant-scoped `PlatformApiKey` can mint a consolidated proxy token for users on its own platform — existing users via `consolidated-token/proxy/`, new users via the new `consolidated-token/provision/` (with edX as the source of truth for identity) — without DM-admin credentials, gated behind IBL-managed flags. The provision endpoint later also returns an edX JWT (`edx_jwt_token`), minted server-to-server from edX via the new `EDX_REQUEST_JWT_PATH` setting.

**Billing and monetization were restructured around Stripe product IDs and a throttle-tiers admin.** A new Throttle tiers admin (with global `free`/`free_trial`/`credits`/`enterprise` tiers and per-platform overrides) replaced six legacy throttle-tier dynamic settings, finalized by a `cleanup_legacy_throttle_settings` management command. `CreditAccount.current_plan`/`previous_plan` are now stored as stable Stripe product IDs (`current_plan_id`/`previous_plan_id`, indexed; migration `0091` backfills and drops the legacy columns), resolving display names against the Throttle tiers registry and making Stripe webhook redeliveries idempotent. A canonical `unique_id`-keyed billing surface (`/api/billing/items/<item_unique_id>/...` plus price-UUID endpoints) lets clients carry one id instead of the nested `(platform_key, item_type, item_id)` triplet, backed by per-request resolution mixins; `billing/urls.py` was split into a modular package and duplicate root-mounting of `billing`/`crm` was removed to clean up the OpenAPI document. Vercel and Apify were seeded as integration credential providers.

**Per-tenant platform migration landed on a vendored import/export manager.** `ibl-dm-import-export-manager` was vendored into the monorepo and enabled in `DM_APPS`, then gained per-tenant platform migration (JSONL export/import, S3-backed bundle storage, resume, deduplication, and admin/REST/management-command interfaces via new `PlatformMigrationRun` and `ImportIdMap` models, gated behind `PLATFORM_MIGRATION_ENABLED`). A `MANIFEST.in` fix made the admin templates load in git-less Docker builds, and the Phase 7 redaction audit was corrected to allowlist the non-secret `connected_services.jsonl -> token_type` (OAuth scheme, not a credential) so clean exports stop failing closed.

**CRM error handling, notification routing, and copy were cleaned up.** `OrganizationSerializer`/`PersonSerializer` now translate uniqueness clashes into `400` field errors instead of `500`s, including the race-window `IntegrityError` path. CRM notification types (`CRM_PERSON_CREATED`, `CRM_DEAL_STAGE_CHANGED`, `CRM_PERSON_LINKED_TO_USER`) moved from learning to nurturing notifications so they route through the centralized nurturing SMTP server, with their template context variables now declared. Separately, a large `main`+`GLOBAL` notification copy pass reworded "mentor" → "agent" and `MentorAI` → `ibl.ai` across 38 template/util files (placeholders, tags, and the `mentor.iblai.app` host preserved), with a light de-education pass. A notification-templates tag-filter `500` (missing `tags` on `CustomNotificationTemplateAdapter`) was fixed, and custom notifications gained a `metadata` JSONField sourcing their tags.

**Models, health checks, and observability.** The deprecated `gemini-2.0-flash` default was replaced by a deployment-configurable `gemini-3-flash-preview` via a new `DEFAULT_GOOGLE_FLASH_MODEL` setting threaded through every hardcoded call site, and `GPT_5_MODELS` was expanded with current gpt-5.x identifiers. Guided-prompt generation was split into `SystemMessage` + `HumanMessage` so it no longer trips Azure's jailbreak prompt shield. The agent health check now exercises the real chat-write path (a rolled-back `Session`/`ChatMessageHistory` insert probe) so schema/code skew surfaces as `503` instead of staying green. Memsearch auto-capture was fixed to honor model defaults on first access by switching four reads to `aget_or_create`. Content analytics (`/api/analytics/content/`) now always includes `active_courses`/`active_programs` in platform-scoped totals, with `active_courses` counting distinct courses that have an active enrollment. Template-agent lookups got a partial index `mentor_template_lookup_idx` (built `CONCURRENTLY`) to stop sequential-scanning a tenant's agent table, and `mentor-with-settings` was fixed to honor `forkable_with_training_data` and to tear down orphaned agents when post-create serialization fails.

**Google Drive ingestion scope was narrowed, then reverted.** The document-ingestion OAuth scope was tightened from `drive.readonly` to `drive.file` to match Google's verification guidance, but this broke ingestion of existing user-owned files, so it was reverted to `drive.readonly`; tenants that re-authorized under `drive.file` must re-issue tokens.

**Security hardening.** An Orca CVE remediation pass on the DM image raised patched versions for prod-confirmed-open dependencies — `cryptography` 49.0.0 (with `cffi` 2.0.0 / `pyOpenSSL` 26.0.0 / `PyNaCl` 1.6.2), `urllib3` 2.7.0, `aiohttp` 3.14.1, `authlib` 1.7.2, `lxml` 6.1.1, `pypdf` 6.14.2, `python-multipart`, `ujson`, `GitPython`, `jwcrypto`, and `requests` — un-pinning `requests` in `ibl-dm-axd-app` so the bump resolves and fixing a runtime-pin clobber. A late fix to `RecentlyAccessedMentorsView` restored cross-tenant visibility of starred/recent public agents that live on the `main` tenant, evaluating the RBAC filter against the main platform's resource path.

## `iblai-os-spa` — `0.70.1` → `0.88.2`

**The OS SPA is the renamed agent app.** The `spa.agent` surface was renamed end-to-end to `os` (served at `os.<base>`), sharing the `iblai-mentor-spa-pro` image, and the in-app product name changed from agentAI to OS with `iblai.org` default URLs migrated to `iblai.app`; backward-compat 301 redirects route `agent.ibl.ai` → `os.ibl.ai`.

**Streaming reasoning and tool-call visibility.** The chat surface added a `ReasoningSection` (auto-open/collapse) and a `ToolCallIndicator` / `ToolCallItem` (collapsible details, unique-tool counting) inside `AIMessageBubble`, streaming reasoning content and tool calls out of `additional_kwargs`. The whole reasoning + tool-call UI is gated on a "Verbose Reasoning" toggle (renamed from "Show Reasoning") in agent settings, which later releases hardened to respect RBAC. Streaming polish included hiding the action toolbar and empty bubbles while a message streams and restoring them on completion.

**Private/temporary chat.** A private (temporary) chat mode was added with a reorganized, categorized privacy/settings tab, working across embed and small screens, with the privacy toggle moved into settings.

**Chat input and history.** Users can paste text and files directly into uploads (clipboard file-extraction helper, attachment chips showing the full MIME type), chat history paginates with infinite scroll-up, and keyboard focus stays on the message textarea while chatting (WCAG-576) rather than jumping to the stop/copy buttons. A prompt-caching toggle and a configurable copy-character limit (`NEXT_PUBLIC_MAXIMUM_CHARACTER_SIZE_TO_COPY`) were added to agent settings, along with an "Optimize Page Context Tokens" embed toggle and a streamable text-to-speech endpoint.

**New surfaces.** A dedicated Projects page route was added (sourced from the SDK, with agentless-project navigation blocked), the app was internationalized with `next-intl`, a Chrome side-panel extension hosting `<agent-ai>` shipped, and a dataset-curator/viewer role was exposed on the access tab. The agent picker and Explore agents section were unified onto the `ai-search` endpoint.

**Desktop and mobile app.** The Tauri desktop build gained GhostOS support, new Ollama backend functions, model-manager installation status re-polling, usable RAM/VRAM checks with program-install fallbacks, a macOS DMG release, and MSIX/Store packaging fixes; deep-link handling rewrites custom-scheme (`iblai-mentor://`) URLs back into the webview and routes Android SSO deep links back into the app.

**Copy and RBAC.** Education-specific copy was genericized — "Students" visibility/labels became "Users", the "Student Success" tools category was renamed, and marketing/explore copy dropped "learning" phrasing. The "Add Resource" button is now hidden without documents-create permission, support-phone display sits behind an env flag, and the Google Drive picker was patched to drop its hardcoded `drive.readonly` scope. Late-June work patched `vitest`/`dompurify`/`@opentelemetry/core` vulnerabilities and added dependency vulnerability scanning to the PR pipeline.

## `iblai-lms-spa` — `0.36.0` → `0.43.2`

**The LMS SPA is the renamed Skills app.** The `spa.skillsai` surface was renamed to `lms` (served at `lms.<base>`, sharing the `iblai-skills-spa-pro` image), the Tauri app was renamed to Agentic LMS, the deployment URL moved to `https://lms.ibl.ai`, and `skillsai.ibl.ai` → `lms.ibl.ai` 301 redirects were added. Early June also saw an extended run of dependency-security overrides (axios, hono, postcss, dompurify, basic-ftp, brace-expansion, and Cargo deps for the Tauri shell) plus an OAuth popup for the desktop build.

**Public catalog and access.** A public course catalog gained login/signup buttons, home/discover/recommended pages are hidden when not logged in, and course-metadata content-mode targeting was implemented. AI-analytics buttons are now visible to holders of view-analytics and watcher RBAC permissions, with analytics layout height restructured and an audit analytics page added.

**Reading and layout.** The course outline became collapsible on tablet and small-PC screens (appearing uncollapsed, dropping the tooltips), the default sidebar-agent load logic was aligned with the OS app, and grade info no longer appears in the education panel when unset.

**Platform parity.** Android SSO deep-link support was added with an aligned package id, a support-phone toggle was implemented behind an env update, and the final June release delivered an overall SPA optimization pass (lazy loading and re-render fixes, with a `next.config.mjs` update).

## `iblai-edx-pro` — `sumac.2.32.0` → `sumac.2.36.0`

**Create courses on behalf of another user.** `POST /api/v1/ibl/course/create` accepts an optional `on_behalf_of` username so a staff/superuser caller (e.g. an automation service account) can create a course owned by another user, backed by new `ibl-edx-course-creator` helpers (`resolve_on_behalf_of_user` staff-gate and `grant_course_authoring_access` granting `CourseInstructorRole`+`CourseStaffRole`); the target user only needs org membership on this path while the legacy self path still requires platform admin.

**Server-to-server JWT minting.** `GET /ibl-auth/request-jwt/` gained a second mode: with a `username`/`email` query param, a trusted server (e.g. Data Manager) can mint an edX JWT for a named user without that user's browser session, gated on an OAuth-app allowlist (`IBL_S2S_JWT_ALLOWED_OAUTH_APPS`, default `["ibl_edx"]`) and a staff+superuser check (`IBL_S2S_JWT_REQUIRE_ADMIN`); with no identifier the existing self-service flow is byte-for-byte unchanged.

**Studio boot fix.** Studio (CMS) had failed to boot with a `RuntimeError` (`InvalidToken` model missing an `app_label`) because the disposable-email-blocking refactor eagerly imported `ibl_edx_account_api.models` at URL-load time while the app was only registered for the LMS; adding a `cms.djangoapp` entry point (models-only, no CMS URLs/settings) registers it in Studio's `INSTALLED_APPS`. The Studio content-mode target-user dropdowns were also renamed to plurals (`Learners`/`Watchers`/`Admins`).

**A second, independent OIDC SSO backend.** `IBLOpenIDOAuthSSOBackend` (`ibl_base_oauth_sso_backend.auth_openid`) is a behavioral clone of the base backend reading a parallel `IBL_OPENID_SSO_*` / `IBL_OPENID_USE_IDP_USERNAME` namespace, so a deployment can run a second, fully independent OIDC integration alongside the existing `IBL_OAUTH_SSO_*` one; it stays inert until its settings and a matching `OAuth2ProviderConfig` are configured. The companion `ibl-cli-ops` 5.37.0 renders these settings and registers the backend in `THIRD_PARTY_AUTH_BACKENDS`.

**Large-callback login fix.** The uWSGI `buffer-size` was raised to `65536` so large OAuth2 callbacks no longer 502 — confirmed on Syracuse Azure AD, whose ~8.4k request line exceeded the `8192` the Sumac image had regressed to when uWSGI launch moved from `run.sh` to a static `uwsgi.ini`; it now matches the nginx inbound ceiling (64k).

**Security hardening.** An Orca CVE remediation pass on the Sumac image bumped Python deps (aiohttp, nltk, Jinja2, Mako, pillow, lxml 6.1.1 + `lxml-html-clean`, protobuf, sqlparse, requests, PyJWT, and more), re-applied runtime pins last in the production stage so they can no longer be clobbered (urllib3 had shipped at 2.2.3 despite a 2.7.0 pin), upgraded the crypto stack (`cryptography` 46.0.5, `pyOpenSSL` 26.0.0 closing CRITICAL CVE-2026-27459) by dropping the unused `snowflake-connector-python`, and pulled current Ubuntu 22.04 OS security patches. Known deferrals (`pymongo`, `social-auth-app-django`, and bundled edx-platform npm) are hard-blocked upstream.

## `iblai/infra-cli` — `1.11.0` → `1.12.0`

**Optional AWS WAFv2 on the single-server ALB.** `1.11.0` added opt-in WAFv2 at provision time (wizard sub-step, `provision-env` `ENABLE_WAF`/`WAF_ALLOWED_IPS`, or `launch`/`launch-env` flags), attaching a Regional Web ACL tuned for ibl.ai's subdomain layout: operator-IP-gated admin allow rules for DM Swagger, edX Studio, Django `/admin/`, and DM `/data`; a public allow rule for `learn.<base>`; six AWS managed rule groups; and a path-traversal block (`.git`/`.env`/`.htaccess`/etc.), totaling ≈1355 WCU. A new `iblai infra waf` post-provision subgroup (`enable` / `enable-env` / `disable` / `status`) toggles it on an already-provisioned stack without re-running the wizard, and a shared `TerraformRunner.reapply()` helper (which pins the original `bucket_suffix` to prevent accidental S3 bucket renames) now backs both the WAF commands and a refactored `iblai infra retry`. WAFv2 entries were added to `REQUIRED_IAM_POLICY` with a `wafv2:ListWebACLs` smoke check so `iblai infra permissions --check` surfaces readiness up-front.

**Bootstrap and SSO fixes.** `1.12.0` fixed DM bootstrap to run `CREATE EXTENSION IF NOT EXISTS timescaledb` right after pgvector — the DM postgres image preloads TimescaleDB but the flow had only ever created `vector`, so `setup_timescale_views --full-setup` silently degraded and analytics hypertables were never built; the blanket `ignore_errors: true` on that task was replaced with `register` + `failed_when` so genuine tracebacks now surface. Microsoft SSO was fixed to use the standard `azuread-oauth2` backend across every reference (`OAuth2ProviderConfig.backend_name`, the `IBL_EDX_BASE_OAUTH_SSO_BACKEND` block, `other_settings.backend_uri`, and `IBL_SPA.AUTH.IBL_DIRECT_SSO_URL`) instead of deriving a bogus `<platform_name>-oauth2` slug that prevented sign-in; `IBL_DM.ENABLE_RBAC_GROUP_MANAGEMENT=true` is now set by default, and the exact Azure AD redirect URI (`https://learn.<BASE_DOMAIN>/auth/complete/azuread-oauth2/`) is documented in `.env.setup.example` and the end-of-run confirmation.

**Companion catalog operations (via the bundled `ibl-cli-ops` pin).** Across June the prod-images catalog advanced its `ibl-cli-ops` pin from `5.24.0` to `5.38.0`, shipping operator-facing tooling alongside the SPA renames: `ibl spa restart [SPA ...]` with implicit derivative pairing (restarting `mentor` also restarts `os`; `skills` also restarts `lms`), CORS headers for the `/ibl-auth/get-sso-login-url/` SSO endpoint, `NEXT_PUBLIC_ENABLE_SUPPORT_PHONE` and `NEXT_PUBLIC_MAXIMUM_CHARACTER_SIZE_TO_COPY` env plumbing for the SPAs, and an opt-in `pg-healthcheck` per-node Postgres activity/lock sampler that surfaces the offending query and PID during a spike, exposing low-cardinality Prometheus gauges (`pg_blocked_backends`, `pg_longest_active_query_seconds`, …) and four alerts without query text ever entering Prometheus.
