Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Stability markers

rolter ships a wide surface, and not all of it is equally finished. Until #1385 nothing distinguished a subsystem an operator could build a deployment on from one that is a first cut: every screen and every endpoint presented with the same confidence, so the only way to find the thin parts was to read the source.

A stability marker says which parts are still moving. There are two levels:

LevelMeans
stablethe default. Covered by whatever compatibility promise the release makes
experimentalmay change shape or be removed in a minor release

The source of truth is SUBSYSTEMS in crates/rolter-core/src/stability.rs. Nothing re-lists it: the control plane serves it, the dashboard renders it, and a unit test in that module fails if the table below stops matching.

What the marker promises

experimental is a documented exemption, and the exemption is the whole point. It withholds exactly one guarantee: that the subsystem will still have the same shape, or exist at all, in the next minor release. Concretely, on an experimental subsystem a minor release may

  • rename, restructure or withdraw its API paths, request and response fields;
  • rename or drop its configuration keys and environment variables;
  • change what its stored rows mean, or stop reading them;
  • remove its dashboard screen.

None of that is licensed on a stable subsystem.

What the marker does not say

This is the half that gets misread, so it is stated positively:

  • It is not a quality warning. An experimental subsystem is not expected to crash, leak or lose data. It is held to the same review, test and security standards as everything else. The claim is about the design settling, not about the code being shaky — which is why the word is “experimental” and not “unstable”.
  • It is not a support disclaimer. A bug in an experimental subsystem is a bug and gets fixed.
  • It is not a schedule. Nothing here graduates on a date, and an entry may sit for several releases or be withdrawn instead. That is why the word is not “beta”, which reads as a time-boxed pre-release.
  • It is not “off”. Whether a subsystem runs is a different question entirely; see the axes below. Every subsystem in the table is enabled and usable in a default deployment.

The third axis

Stability composes with the two axes ADR-0031 proposes and is deliberately not a third mechanism inside them:

AxisQuestionSet byWhere it lives
capabilitycan this deployment run it?the build and its infrastructureunavailable_flags(), read-only
enablementis it turned on?the operatora stored flag, hot-reloaded through /internal/snapshot
stabilityhow finished is it?us, at build timeSUBSYSTEMS, a const

The three are independent. An experimental subsystem can be perfectly available and perfectly enabled, and marking one experimental never turns anything off.

The reason stability is a compile-time constant and not a database row is that it is a property of the code an operator received. If it were editable it could be set to a claim the running binary does not support, and it would have to be propagated, versioned and audited like any other config — for a value that can only change by upgrading.

ADR-0031 is still proposed at the time of writing, so this page does not depend on its code. It depends on its vocabulary, which is the part worth keeping consistent.

What is experimental in this build

Each entry earns its place by pointing at a gap the tree can be checked against — a documented “not yet”, a management surface the data plane does not read, an enforcement path a request can take without meeting. Not by impression.

SubsystemDashboardWhy it is experimental
labels(no screen; chips on Providers, Provider Groups and Routing Rules)Display and filter only. A route cannot select its targets by label, and making labels selectable turns them into configuration the data plane consumes — a change in what a label is. Stated in Labels.
mcp_settingsMCP → MCP SettingsThe screen stores organization defaults for transport, timeout intent, retries, failure policy and undeclared tools, and the HTTP proxy does not read them. Since #952 it does read a per-server override; a server without one falls back to the deployment-level transport timeouts rather than to these org defaults.
mcp_tool_groupsMCP → Tool GroupsTool-group manifests are stored and published to MCP-aware clients, but the proxy does not enforce group membership as an access boundary. Access is still decided by virtual-key owner, server, the server’s configured credential — a static one, or a live OAuth session and its required scopes — and nothing about which tools a group names. Stated in MCP OAuth.
pluginsPluginsThe webhook payload a plugin receives carries no version of its own, so the dispatch contract cannot change without silently breaking every endpoint already written against it. PluginRequest in plugin_dispatch.rs is the shape in question.
realtime(no screen; reachable from the Playground)Guardrails, the PII sanitizer and plugins do not run on /v1/realtime session events. A session is admitted against budgets and rate limits and metered per response turn (Realtime metering), but what it carries reaches the provider without a content check. Tracked in #1880.
skills_repositorySkills RepositoryA skill resolves only through the control-plane API; the gateway serves no skill surface, so how a client addresses and fetches one is not settled. Nothing outside the dashboard depends on the current shape yet.

A subsystem not in that table is stable. The table is short deliberately: a marker on every page is a marker nobody reads, and the value of this one is entirely in how rarely it appears.

Adding, changing or removing a marker

  1. Add, edit or delete the SubsystemStability row in crates/rolter-core/src/stability.rs. Keep the list sorted by id; the id is published and is never renamed once it has shipped.
  2. Update the table above in the same change. The the_docs_page_lists_exactly_these_subsystems test compares the two lists row for row, so a marker cannot ship without the paragraph that explains what it exempts the subsystem from.
  3. If the subsystem has a docs/user-docs/ page, put the note at the top of it, in the reader’s words rather than these.
  4. nav_keys are leaf keys from NAV in ui/src/lib/nav.tsx, checked by every_nav_key_is_claimed_once_and_exists_in_the_dashboard_nav. Leave it empty when the subsystem has no screen of its own; that is normal, not a gap.
  5. Add the note to the dashboard catalogs as stability.notes.<id> in ui/src/lib/i18n/locales/en.json and every sibling catalog, and drop it when the row goes. ui/scripts/stability-notes.test.ts (part of bun run test) fails while the en keys and the SUBSYSTEMS ids disagree, and bun run check:i18n holds every other locale to the en key set.

Why the dashboard does not render note

note on the wire is English, and a dashboard running in Russian used to show the translated word Экспериментально with a sentence of English behind it (#1401). The rail therefore uses only the id from the answer and looks the note up in the catalogs, where every other string it shows already lives. The list itself keeps one owner — which subsystems are marked, and on which nav entries, still comes from the control plane.

An id the dashboard’s catalogs do not know (in practice only a control plane newer than the bundle it serves) still gets its marker, just without the explanation. Nothing falls back to the English note: the marker carries the level, and the note is supplementary.

Graduating a subsystem is a deliberate act. Deleting a row says the shape is now something we will not change in a minor release, so it belongs in the pull request that closes the last gap the note names — not in a tidy-up sweep.

Granularity: why per subsystem

Per endpoint is more precise and rots faster: a route rename silently drops the marker, and a subsystem’s surface is usually several routes plus a screen plus a config block that all move together. Per dashboard screen cannot express anything without a screen, such as labels and realtime above.

Per subsystem is the same unit ADR-0031 uses for capability and enablement, so all three axes answer for the same thing. nav_keys then carries the mapping onto dashboard nav entries, one owner, zero, one or many keys per subsystem.

Where it is served

GET /api/v1/stability:

[
  {
    "id": "labels",
    "stability": "experimental",
    "note": "labels are display and filter only: no route can select its targets by label yet, …",
    "nav_keys": []
  }
]

Only the exceptions travel. A subsystem absent from the array is stable, which is what the dashboard renders nothing for — absence is the signal, and "stability": "stable" is never sent. An empty array is the healthy answer for a release with nothing experimental left in it.

It is its own endpoint rather than a field on an existing payload because the marker is read by a different question than any of them answers: feature-flags is superadmin-only and about enablement, and version is about the update check. The capability is stability:read, held by every authenticated caller including a viewer with no membership anywhere — the nav rail is rendered for everyone who can sign in, and a marker the least-privileged user cannot fetch is missing exactly where the reassurance is worth most. It names no tenant’s data, only what this build ships. The operation is getStability in the control-plane OpenAPI document, with a SubsystemStability schema, so an SDK author sees it too.

Relationship to the 1.0.0 guarantees (#922)

ADR-0032 decides what each stable surface guarantees at 1.0 — the /v1/* gateway surface, the /api/v1/* control API, the config keys and environment variables, the database schema and the Rust crates. This page does not restate it. It is named there as the exemption route: a subsystem carrying the experimental marker is outside every promise the ADR makes, and no second mechanism exists beside it.

What this page does is make that table writable. A 1.0 that promises compatibility across the whole surface either over-promises or is held hostage to the least-finished corner of the product; naming the exemptions is the honest, cheap third option. Each row above is one line #922 does not have to argue about. API stability and the semver gate is the same argument for the Rust crates.