feat(server): add deployment-wide release feature flags (#4775)

## Related issue

Follow-up to #4673.

## Summary

- Add a typed, default-off release-feature registry driven by one comma-separated `OMNIGENT_FEATURES` environment variable, with strict validation and lifecycle metadata.
- Gate the web Usage route/navigation and page-only report enrichment while preserving the existing `GET /v1/usage` CLI API.
- Migrate web-driven harness installation to the same immutable startup snapshot and wire rollout configuration across Docker, Kubernetes, Render, Railway, and Databricks.

ELI5: the server reads one list of enabled features when it starts, enforces that same list on backend routes, and tells the web app which controls and pages to show.

```text
OMNIGENT_FEATURES
        |
        v
  FeatureFlags snapshot
     /             \
backend gates    GET /v1/info
                       |
                       v
                 frontend gates
```

## Test Plan

- `uv run pytest tests/server/test_feature_flags.py tests/host/test_local_server.py tests/server/integration/test_utility_endpoints.py tests/server/integration/test_hosts_install_harness.py tests/server/integration/test_hosts_store_credential.py tests/server/routes/test_usage_report.py tests/server/test_openapi_drift.py -q`
- `cd web && pnpm vitest run src/lib/capabilities.test.ts src/lib/harnessSetup.test.ts src/App.test.tsx src/shell/Sidebar.test.tsx`
- `uv run pytest tests/e2e_ui/sessions/test_usage_page_feature.py -q`
- `uv run python scripts/dump_openapi.py --check`
- `pre-commit run --files <changed files>`
- Verified default-off and enabled Usage route/sidebar behavior, strict unknown-feature rejection, legacy CLI usage compatibility, and harness route enforcement.

## Demo

- Default off: the updated visual baselines show the original sidebar without the Usage row.
- Enabled Usage page: https://github.com/user-attachments/assets/8385d4f0-47ad-430f-bf2c-06c35af6c499

## Type of change

- [ ] Bug fix
- [x] Feature
- [x] UI / frontend change
- [ ] Refactor / chore
- [x] Docs
- [ ] Test / CI
- [ ] Breaking change

## Test coverage

- [x] Unit tests added / updated
- [x] Integration tests added / updated
- [x] E2E tests added / updated
- [x] Manual verification completed
- [x] Existing tests cover this change
- [ ] Not applicable

## Coverage notes

Manually reviewed the default-off visual output and verified that the Usage route is absent while the capability is disabled. Targeted backend and frontend tests cover both flag states, capability parsing, startup snapshots, and harness enforcement.

## Changelog

Usage and web-driven harness setup can now be enabled per deployment with `OMNIGENT_FEATURES`.

Signed-off-by: Zeyi (Rice) Fan <zeyi.f@databricks.com>
This commit is contained in:
Zeyi (Rice) Fan
2026-08-14 10:57:20 -07:00
committed by GitHub
parent 8f194d6f9d
commit b9d53f0a96
48 changed files with 806 additions and 145 deletions
+11
View File
@@ -147,6 +147,17 @@ UC Volume wheel paths because `uv lock` validates path sources locally.
Re-running is safe — every step is idempotent.
Release features are off by default. Enable one or more for the whole app by
adding the comma-separated deploy argument, then reload the web app after the
redeploy:
```bash
--features usage_page,harness_install
```
See [`designs/FEATURE_FLAGS.md`](../../designs/FEATURE_FLAGS.md) for the current
inventory and rollback procedure.
> [!TIP]
> To lock against a private PyPI mirror or proxy instead of public
> PyPI, set `UV_INDEX_URL` before running `deploy.py`.
+5
View File
@@ -21,6 +21,9 @@ variables:
UC schema (catalog.schema) holding the OTel destination tables.
The platform writes to <schema>.otel_logs, otel_metrics, otel_spans.
default: main.omnigent_logs
features:
description: "Comma-separated deployment-wide release features."
default: ""
resources:
apps:
@@ -44,6 +47,8 @@ resources:
value_from: artifact_volume
- name: OTEL_TRACES_SAMPLER
value: 'always_on'
- name: OMNIGENT_FEATURES
value: "${var.features}"
resources:
- name: postgres
postgres:
+10
View File
@@ -567,6 +567,14 @@ def _parse_args() -> argparse.Namespace:
"<schema>.otel_{logs,metrics,spans}."
),
)
parser.add_argument(
"--features",
default="",
help=(
"Comma-separated deployment-wide release features, e.g. "
"'usage_page'. Empty keeps every release feature off."
),
)
parser.add_argument(
"--target",
default="prod",
@@ -746,6 +754,8 @@ def _bundle_vars(args: argparse.Namespace) -> list[str]:
f"volume_name={args.volume_name}",
"--var",
f"otel_table_schema={args.otel_table_schema}",
"--var",
f"features={args.features}",
]
+6
View File
@@ -15,6 +15,12 @@ POSTGRES_PASSWORD=change-me-please
# Host port the omnigent container is published on. Default 8000.
# OMNIGENT_PORT=8000
# ── Release features ─────────────────────────────────────
# Comma-separated deployment-wide release features. Empty/unset keeps every
# release feature off. Unknown names fail startup so typos cannot silently
# change rollout behavior. Current keys: usage_page, harness_install.
# OMNIGENT_FEATURES=usage_page
# ── Image ────────────────────────────────────────────────
# The compose stack pulls a pre-built image from GHCR (built by CI on
# every main-branch merge). Default: ghcr.io/omnigent-ai/omnigent-server.
+21
View File
@@ -39,6 +39,27 @@ Reset everything (drops the DB and the artifact store):
docker compose down -v
```
## Release features
Release features are deployment-wide and off by default. Enable one or more
with the comma-separated `OMNIGENT_FEATURES` variable in `.env`, then recreate
the server container:
```dotenv
OMNIGENT_FEATURES=usage_page
```
```bash
docker compose up -d
curl -s http://localhost:8000/v1/info | jq '.features'
```
Known keys and their lifecycle are documented in
[`designs/FEATURE_FLAGS.md`](../../designs/FEATURE_FLAGS.md). Unknown keys fail
server startup so a typo cannot silently produce the wrong rollout. To roll
back, remove the key (or empty the variable), run `docker compose up -d` again,
and reload the web app.
## Multi-user mode (accounts — default)
Built-in accounts auth: no IdP to register, no proxy to host.
+3
View File
@@ -62,6 +62,9 @@ services:
ARTIFACT_DIR: /data/artifacts
HOST: 0.0.0.0
PORT: "8000"
# Comma-separated deployment-wide release features. Empty means all
# release features are off; see .env.example for the known keys.
OMNIGENT_FEATURES: "${OMNIGENT_FEATURES:-}"
# Anchor the server's data dir on the persistent volume so
# file-backed operator config survives container restarts:
# the admin roster (/data/admins) and allowed-domains file
+16
View File
@@ -91,6 +91,22 @@ Apply your chosen issuer with `kubectl apply -f <file>`. Without it, cert-manage
logs `IssuerNotFound` and no certificate is issued (the server still runs — only
TLS is affected).
## Release features
Release features are deployment-wide and off by default. Set the
comma-separated `OMNIGENT_FEATURES` value in `base/configmap.yaml`, apply your
Kustomize target, and restart the Deployment so every pod receives one fresh
startup snapshot:
```bash
kubectl kustomize deploy/kubernetes/base/ | kubectl apply -f -
kubectl rollout restart deployment/omnigent
kubectl rollout status deployment/omnigent
```
Use the same restart after removing a feature for rollback. See
[`designs/FEATURE_FLAGS.md`](../../designs/FEATURE_FLAGS.md) for known keys.
## Deploy with an external database
Use this path when you have a managed Postgres (RDS, Cloud SQL, Neon, etc.).
+2
View File
@@ -8,6 +8,8 @@ data:
HOST: "0.0.0.0"
PORT: "8000"
ARTIFACT_DIR: "/data/artifacts"
# Comma-separated release features; empty keeps every feature off.
OMNIGENT_FEATURES: ""
OMNIGENT_ADMIN_CREDENTIALS_PATH: "/data/admin-credentials"
OMNIGENT_AUTH_ENABLED: "1"
OMNIGENT_AUTH_PROVIDER: "accounts"
+8
View File
@@ -73,6 +73,14 @@ steps below are validated end-to-end:
> visitor. Pre-seed `OMNIGENT_ACCOUNTS_INIT_ADMIN_PASSWORD`, or complete setup
> promptly after the deploy goes live.
## Release features
In the Omnigent service's **Variables** tab, set `OMNIGENT_FEATURES` to a
comma-separated enabled set such as `usage_page`. Railway redeploys the service
automatically. Remove the key from the value to roll back, then reload the web
app. See [`designs/FEATURE_FLAGS.md`](../../designs/FEATURE_FLAGS.md) for known
keys.
## Use your own IdP instead (OIDC)
Prefer GitHub / Google / Okta login over built-in accounts? Switch the provider
+41
View File
@@ -0,0 +1,41 @@
# Release feature flags
Omnigent release features are deployment-wide, temporary rollout switches. They
are not authorization controls or user preferences.
## Configuration
Set the comma-separated `OMNIGENT_FEATURES` environment variable and restart or
redeploy the server:
```bash
OMNIGENT_FEATURES=usage_page,harness_install
```
Unset or empty means every release feature is off. Unknown names fail startup.
The former `OMNIGENT_HARNESS_INSTALL_ENABLED` switch is rejected with a
migration hint; use `OMNIGENT_FEATURES=harness_install` instead. The server resolves the set once at startup and publishes frontend-visible
values in `GET /v1/info` under `features`. Users must reload the web app after a
flag change because server capabilities are cached at page boot.
`omnigent/server/feature_flags.py` is the source of truth for known keys and
lifecycle metadata.
## Inventory
| Key | Default | Owner | Review by | Purpose |
| --- | --- | --- | --- | --- |
| `usage_page` | Off | Web | 0.11.0 | Exposes the web Usage route, sidebar navigation, timeline, and cost breakdown details. The existing `GET /v1/usage` CLI API remains available while off. |
| `harness_install` | Off | Onboarding | 0.11.0 | Allows the web UI to install or configure supported harnesses on a connected host. |
At the review release, each flag must be removed by making the feature
unconditional, removing the feature, or moving a genuinely permanent operator
policy into normal server configuration.
## Rollout and rollback
1. Deploy an immutable image with the feature absent from `OMNIGENT_FEATURES`.
2. Enable it on one deployment, consistently across all replicas.
3. Verify `GET /v1/info`, then reload and exercise the gated UI.
4. Expand by deployment cohort.
5. Roll back by removing the key and redeploying the same image.
+2 -2
View File
@@ -2888,7 +2888,7 @@ def _foreground_daemon_record(
started_at=int(time.time()),
host_id=host_id,
resolved_server_url=server_url.rstrip("/") if mode == "local" else None,
config_sig=server_config_signature(),
config_sig=server_config_signature(include_features=mode == "local"),
)
@@ -3015,7 +3015,7 @@ def _ensure_host_daemon(server_url: str | None) -> bool:
_persist_spawned_daemon(
target=target,
spawned=spawned,
config_sig=server_config_signature(),
config_sig=server_config_signature(include_features=not server_url),
)
return decision.config_changed
+15 -3
View File
@@ -97,7 +97,7 @@ _LOCAL_SERVER_SIG_PATH = _local_data_dir() / "local_server.sig"
_LOCAL_SERVER_LOG_REF_PATH = _local_data_dir() / "local_server.logpath"
def server_config_signature() -> str:
def server_config_signature(*, include_features: bool = True) -> str:
"""
Compute a signature of the server-affecting config for one invocation.
@@ -111,7 +111,8 @@ def server_config_signature() -> str:
Covers the inputs that change server behavior at spawn time:
* the resolved auth source — auth mode is baked at boot and cannot be
reconfigured in place; and
reconfigured in place;
* the enabled release-feature set — features are snapshotted at boot; and
* the installed package version — a running server holds its code in
memory, so after ``omni upgrade`` (or a manual ``uv tool upgrade``)
the old process keeps serving pre-upgrade code until it is cycled.
@@ -123,6 +124,9 @@ def server_config_signature() -> str:
Deliberately narrow otherwise, so unrelated env churn does not force
needless restarts.
:param include_features: Include server release features. Remote host
daemons connect to a separately managed server, so their signatures
exclude local server features.
:returns: A short hex digest, e.g. ``"3f9a1c2b4d5e6f70"``.
"""
import hashlib
@@ -130,6 +134,7 @@ def server_config_signature() -> str:
import json
from omnigent.server.auth import resolve_auth_source
from omnigent.server.feature_flags import resolve_feature_flags
try:
version = importlib.metadata.version("omnigent")
@@ -138,7 +143,14 @@ def server_config_signature() -> str:
# nothing to key version-drift on, so leave it out of the payload.
version = ""
payload = json.dumps({"auth": resolve_auth_source(), "version": version}, sort_keys=True)
payload = json.dumps(
{
"auth": resolve_auth_source(),
"features": (resolve_feature_flags().enabled_names() if include_features else ()),
"version": version,
},
sort_keys=True,
)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
+16 -11
View File
@@ -48,6 +48,7 @@ from omnigent.server.background_session_titles import (
BackgroundSessionTitleCoordinator,
RunnerBackgroundTitleGenerator,
)
from omnigent.server.feature_flags import Feature, FeatureFlags, resolve_feature_flags
from omnigent.server.managed_hosts import ManagedSandboxDeployment
from omnigent.server.mcp_pool import ServerMcpPool
from omnigent.server.performance_metrics import (
@@ -763,6 +764,7 @@ def create_app(
sharing_mode: SharingMode | Callable[[], SharingMode] | None = None,
public_sharing: bool | Callable[[], bool] | None = None,
server_config: dict[str, Any] | None = None,
feature_flags: FeatureFlags | None = None,
) -> FastAPI:
"""
Build and return the FastAPI application with all routes mounted.
@@ -853,6 +855,9 @@ def create_app(
open to ``ON`` when unset or unrecognized. Reported by
``GET /v1/info`` as ``sharing_mode`` so the web app can gate its
Share controls to match.
:param feature_flags: Optional immutable release-feature snapshot.
When omitted, resolves the comma-separated ``OMNIGENT_FEATURES``
enabled set once at application construction.
:param public_sharing: Whether public (anyone-with-the-link) read
access may be granted — i.e. whether the ``__public__`` grant is
allowed. Orthogonal to ``sharing_mode``: a server can keep normal
@@ -872,6 +877,8 @@ def create_app(
if permission_store is not None and auth_provider is None:
raise ValueError("auth_provider is required when permission_store is provided")
resolved_feature_flags = feature_flags or resolve_feature_flags()
# First-boot admin bootstrap for the accounts auth provider.
# Runs before any route is mounted so the login page is never
# served against an empty user table (avoids the Immich-style
@@ -1185,6 +1192,7 @@ def create_app(
app.state.host_store = host_store
app.state.agent_store = agent_store
app.state.sandbox_config = sandbox_config
app.state.feature_flags = resolved_feature_flags
# Admin roster: the config ``admins:`` list (canonical) union'd with the
# runtime-editable ``<data_dir>/admins`` file. Built once here so BOTH the
# admin-gated auth routes AND ``/v1/me``'s is_admin computation consult the
@@ -1888,17 +1896,10 @@ def create_app(
except ImportError:
smart_routing_enabled = False
smart_routing_sources = {"external": False, "oss": False}
# harness_install_enabled gates the web UI's "Install" action for a
# missing, npm-installable harness on a connected host. Off by default
# (OMNIGENT_HARNESS_INSTALL_ENABLED=1 opts in) while the feature rolls
# out; when false the SPA keeps the prior "run omnigent setup" hint.
# Read live so flipping the env var takes effect without a rebuild.
# The env-var name is shared with the install route so the flag the UI
# sees and the flag the route enforces can never drift apart.
from omnigent.process_logging import env_truthy
from omnigent.server.routes.hosts import HARNESS_INSTALL_ENABLED_ENV
harness_install_enabled = env_truthy(os.environ.get(HARNESS_INSTALL_ENABLED_ENV))
# The immutable feature snapshot is shared by capability
# advertisement and route enforcement, so frontend and backend cannot
# disagree during a process lifetime.
harness_install_enabled = app.state.feature_flags.enabled(Feature.HARNESS_INSTALL)
# installable_harnesses: the exact harness ids the install route accepts
# (bare ids + native spellings resolving to an npm-installable family),
# so the SPA offers setup only where it will succeed and never has to
@@ -1930,6 +1931,8 @@ def create_app(
"server_version": _server_version(),
"smart_routing_enabled": smart_routing_enabled,
"smart_routing_sources": smart_routing_sources,
"features": app.state.feature_flags.frontend_dict(),
# Compatibility fields for frontends predating the feature map.
"harness_install_enabled": harness_install_enabled,
"installable_harnesses": installable_harnesses,
"dictation_available": dictation_available,
@@ -2032,6 +2035,7 @@ def create_app(
create_usage_router(
conversation_store,
auth_provider=auth_provider,
feature_flags=resolved_feature_flags,
),
prefix="/v1",
tags=["usage"],
@@ -2498,6 +2502,7 @@ def create_app(
permission_store=permission_store,
agent_store=agent_store,
agent_cache=agent_cache,
feature_flags=resolved_feature_flags,
),
prefix="/v1",
tags=["hosts"],
+114
View File
@@ -0,0 +1,114 @@
"""Deployment-wide release feature management.
Release features are enabled as a comma-separated set in
``OMNIGENT_FEATURES``. The set is resolved once when an application or route
factory is built, so every request handled by that process sees one immutable
snapshot.
"""
from __future__ import annotations
import os
from collections.abc import Mapping
from dataclasses import dataclass
from enum import StrEnum
FEATURES_ENV_VAR = "OMNIGENT_FEATURES"
_REMOVED_HARNESS_INSTALL_ENV_VAR = "OMNIGENT_HARNESS_INSTALL_ENABLED"
class Feature(StrEnum):
"""Canonical release-feature keys accepted by ``OMNIGENT_FEATURES``."""
USAGE_PAGE = "usage_page"
HARNESS_INSTALL = "harness_install"
@dataclass(frozen=True)
class FeatureDefinition:
"""Lifecycle metadata for one temporary release feature."""
feature: Feature
description: str
owner: str
review_by_release: str
frontend_visible: bool = True
FEATURE_DEFINITIONS: tuple[FeatureDefinition, ...] = (
FeatureDefinition(
feature=Feature.USAGE_PAGE,
description="Web Usage page with cost timeline and breakdowns",
owner="web",
review_by_release="0.11.0",
),
FeatureDefinition(
feature=Feature.HARNESS_INSTALL,
description="Install and configure missing harnesses from the web UI",
owner="onboarding",
review_by_release="0.11.0",
),
)
@dataclass(frozen=True)
class FeatureFlags:
"""Immutable feature values for one server process."""
enabled_features: frozenset[Feature] = frozenset()
def enabled(self, feature: Feature) -> bool:
"""Return whether *feature* is enabled in this snapshot."""
return feature in self.enabled_features
def frontend_dict(self) -> dict[str, bool]:
"""Return every frontend-visible feature and its resolved value."""
return {
definition.feature.value: self.enabled(definition.feature)
for definition in FEATURE_DEFINITIONS
if definition.frontend_visible
}
def enabled_names(self) -> tuple[str, ...]:
"""Return enabled canonical names in stable order."""
return tuple(sorted(feature.value for feature in self.enabled_features))
def resolve_feature_flags(environ: Mapping[str, str] | None = None) -> FeatureFlags:
"""Resolve ``OMNIGENT_FEATURES`` into an immutable feature snapshot.
The variable is a comma-separated enabled set, for example
``usage_page,harness_install``. Unset or empty means every release feature
is off. Unknown names fail startup instead of silently applying a typo.
:param environ: Environment mapping; defaults to :data:`os.environ`.
:returns: Resolved immutable feature values.
:raises ValueError: If the enabled set contains an unknown feature name.
"""
source = os.environ if environ is None else environ
legacy_harness_install = source.get(_REMOVED_HARNESS_INSTALL_ENV_VAR, "").strip().lower()
if legacy_harness_install not in {"", "0", "false", "no", "off"}:
raise ValueError(
f"{_REMOVED_HARNESS_INSTALL_ENV_VAR} is no longer supported; "
f"enable harness installation with {FEATURES_ENV_VAR}=harness_install"
)
raw = source.get(FEATURES_ENV_VAR, "")
names = [name.strip() for name in raw.split(",") if name.strip()]
enabled: set[Feature] = set()
unknown: list[str] = []
for name in names:
try:
enabled.add(Feature(name))
except ValueError:
unknown.append(name)
if unknown:
known = ", ".join(feature.value for feature in Feature)
invalid = ", ".join(sorted(set(unknown)))
raise ValueError(
f"unknown feature(s) in {FEATURES_ENV_VAR}: {invalid}; known features: {known}"
)
return FeatureFlags(frozenset(enabled))
+13 -14
View File
@@ -18,7 +18,6 @@ from __future__ import annotations
import asyncio
import logging
import os
import secrets
from typing import Any
@@ -45,10 +44,10 @@ from omnigent.onboarding.harness_install import (
ui_install_key,
ui_installable_harnesses,
)
from omnigent.process_logging import env_truthy
from omnigent.runner.identity import token_bound_runner_id
from omnigent.runtime.agent_cache import AgentCache
from omnigent.server.auth import AuthProvider
from omnigent.server.feature_flags import Feature, FeatureFlags, resolve_feature_flags
from omnigent.server.host_registry import HostConnection, HostRegistry
from omnigent.server.routes._auth_helpers import require_user
from omnigent.server.routes._host_launch import resolve_host_launch
@@ -80,10 +79,6 @@ _MODEL_OPTIONS_TIMEOUT_S = 15.0
# headroom) keeps a genuine slow install from timing out at the server while
# the host is still succeeding — a "504 but actually installed" outcome.
_INSTALL_HARNESS_TIMEOUT_S = 420.0
# Env var that opts a deployment into the UI harness-install feature (default
# off). Named once here and shared by the route (this file) and the /v1/info
# flag in app.py so the two reads can never diverge on a typo.
HARNESS_INSTALL_ENABLED_ENV = "OMNIGENT_HARNESS_INSTALL_ENABLED"
def _host_absent_error(host: Host) -> OmnigentError:
@@ -554,6 +549,7 @@ def create_hosts_router(
permission_store: PermissionStore | None = None,
agent_store: AgentStore | None = None,
agent_cache: AgentCache | None = None,
feature_flags: FeatureFlags | None = None,
) -> APIRouter:
"""Build the router for host REST endpoints.
@@ -574,8 +570,11 @@ def create_hosts_router(
:func:`omnigent.server.app.create_app` always supplies it.
:param agent_cache: Agent-spec cache used to read the agent's
``os_env.cwd`` boundary. Paired with ``agent_store``.
:param feature_flags: Immutable deployment release-feature snapshot.
When omitted, resolves ``OMNIGENT_FEATURES`` at router construction.
:returns: A FastAPI router with host endpoints.
"""
flags = feature_flags or resolve_feature_flags()
router = APIRouter()
@router.get("/hosts")
@@ -1256,8 +1255,8 @@ def create_hosts_router(
may install onto it. Scoped to the UI-installable allowlist (claude,
codex, pi, opencode, qwen) — curl/brew and interactive-auth harnesses
are refused. The whole route is gated behind
``OMNIGENT_HARNESS_INSTALL_ENABLED`` (default off): when disabled it
returns 404 so the feature is invisible until opted in.
``harness_install`` in ``OMNIGENT_FEATURES`` (default off): when
disabled it returns 404 so the feature is invisible until opted in.
Concurrent requests for the same (host, harness) coalesce onto one
in-flight install so a double-click can't fire two global npm installs.
@@ -1275,9 +1274,9 @@ def create_hosts_router(
caller is not the host owner, 409 when the host is offline, 502 on
a host-side install failure, 504 on host timeout.
"""
# Feature flag (default off): a disabled route is indistinguishable
# from a non-existent one, so the feature is fully dark until opted in.
if not env_truthy(os.environ.get(HARNESS_INSTALL_ENABLED_ENV)):
# A disabled route is indistinguishable from a non-existent one, so
# the feature is fully dark until the deployment opts in.
if not flags.enabled(Feature.HARNESS_INSTALL):
raise HTTPException(status_code=404, detail="not found")
# Allowlist (400) is checked before the ownership check (403) so error
@@ -1367,7 +1366,7 @@ def create_hosts_router(
Backs the Web UI setup dialog's "Add a credential" action so a user can
configure a Claude / Codex / Pi credential on a connected host without a
terminal. Owner-scoped, allowlisted, and gated behind
``OMNIGENT_HARNESS_INSTALL_ENABLED`` exactly like the install route
``harness_install`` release feature exactly like the install route
(404 when disabled). The host daemon does the write with the same
non-interactive core the ``omnigent setup`` wizard uses.
@@ -1391,7 +1390,7 @@ def create_hosts_router(
the owner, 409 when offline, 502 on host-side failure, 504 on
timeout.
"""
if not env_truthy(os.environ.get(HARNESS_INSTALL_ENABLED_ENV)):
if not flags.enabled(Feature.HARNESS_INSTALL):
raise HTTPException(status_code=404, detail="not found")
# Allowlist before ownership (403) so error codes can't enumerate
@@ -1484,7 +1483,7 @@ def create_hosts_router(
:raises HTTPException: 404 when disabled or host unknown, 403 when not
the owner, 409 when offline, 502/504 on host failure/timeout.
"""
if not env_truthy(os.environ.get(HARNESS_INSTALL_ENABLED_ENV)):
if not flags.enabled(Feature.HARNESS_INSTALL):
raise HTTPException(status_code=404, detail="not found")
user_id = require_user(request, auth_provider)
+27 -5
View File
@@ -12,6 +12,7 @@ from omnigent._wrapper_labels import WRAPPER_LABEL_KEY
from omnigent.entities import Conversation
from omnigent.runtime.policies.builder import load_session_usage
from omnigent.server.auth import RESERVED_USER_LOCAL, AuthProvider
from omnigent.server.feature_flags import Feature, FeatureFlags, resolve_feature_flags
from omnigent.server.routes._auth_helpers import require_user
from omnigent.server.routes._sessions.helpers import (
_resolve_harness_impl,
@@ -100,6 +101,8 @@ def _session_cost(usage: dict[str, Any]) -> float:
def _build_usage_report(
conversation_store: ConversationStore,
user_id: str | None,
*,
include_page_details: bool = False,
) -> UsageReport:
"""
Build the usage report: a daily-rollup cost summary plus session detail.
@@ -117,6 +120,8 @@ def _build_usage_report(
:param conversation_store: Store to read the rollup and sessions from.
:param user_id: The caller / ACL scope. ``None`` in single-user mode maps
to the reserved local owner the daily rollup and grants are keyed by.
:param include_page_details: Populate the timeline and display metadata
used only by the release-gated web Usage page.
:returns: The populated :class:`UsageReport`.
"""
# The daily rollup and session-permission grants key spend by the resolved
@@ -155,16 +160,24 @@ def _build_usage_report(
title=conv.title,
cost_usd=_session_cost(usage),
models=_session_models(usage),
harness=_resolve_session_harness(conv),
llm_model=conv.model_override or _resolve_llm_model(conv),
agent_name=conv.sub_agent_name,
harness=_resolve_session_harness(conv) if include_page_details else None,
llm_model=(
conv.model_override or _resolve_llm_model(conv)
if include_page_details
else None
),
agent_name=conv.sub_agent_name if include_page_details else None,
)
)
if not page.has_more:
break
after = page.last_id
daily_costs_raw = conversation_store.list_daily_costs(rollup_user, _EPOCH_DAY)
daily_costs_raw = (
conversation_store.list_daily_costs(rollup_user, _EPOCH_DAY)
if include_page_details
else []
)
return UsageReport(
cost_today=cost_today,
@@ -180,6 +193,7 @@ def create_usage_router(
conversation_store: ConversationStore,
*,
auth_provider: AuthProvider | None = None,
feature_flags: FeatureFlags | None = None,
) -> APIRouter:
"""
Create the per-user usage-report router.
@@ -190,8 +204,11 @@ def create_usage_router(
:param conversation_store: Store for the daily rollup and session reads.
:param auth_provider: Auth provider for user identity. ``None`` disables
auth (single-user / local mode).
:param feature_flags: Immutable deployment release-feature snapshot.
When omitted, resolves ``OMNIGENT_FEATURES`` at router construction.
:returns: The configured router (mounted under ``/v1``).
"""
flags = feature_flags or resolve_feature_flags()
router = APIRouter()
@router.get("/usage", response_model=UsageReport)
@@ -205,6 +222,11 @@ def create_usage_router(
``None`` only when auth is disabled — the single-user / local case).
"""
user_id = require_user(request, auth_provider)
return await asyncio.to_thread(_build_usage_report, conversation_store, user_id)
return await asyncio.to_thread(
_build_usage_report,
conversation_store,
user_id,
include_page_details=flags.enabled(Feature.USAGE_PAGE),
)
return router
+2 -2
View File
@@ -7691,7 +7691,7 @@
},
"/v1/hosts/{host_id}/harnesses/{harness}/credential": {
"post": {
"description": "Write a harness provider credential onto a connected host.\n\nBacks the Web UI setup dialog's \"Add a credential\" action so a user can\nconfigure a Claude / Codex / Pi credential on a connected host without a\nterminal. Owner-scoped, allowlisted, and gated behind\n`OMNIGENT_HARNESS_INSTALL_ENABLED` exactly like the install route\n(404 when disabled). The host daemon does the write with the same\nnon-interactive core the `omnigent setup` wizard uses.\n\nSecurity: the server is an authz'd pass-through \u2014 it validates\nownership + the allowlist and forwards the secret over the (TLS) tunnel;\nit never persists the secret or logs it. The secret rides in the request\nbody (not the URL), and the frame field is redaction-named so it never\nlands on a telemetry span.\n\n**Parameters**\n\n- `body` \u2014 The credential payload (kind + secret / gateway / adopt).\n\n**Returns:** `{\"object\": \"harness_credential\", \"harness\": ..., \"configured_harnesses\": {...}, \"gateway_inference\": {...} | None}` \u2014 refreshed readiness so the UI can flip the badge without a reconnect, plus the refreshed gateway-inference map (`None` when the host didn't report one).\n\n**Raises**\n\n- `HTTPException` \u2014 404 when disabled or host unknown, 400 when the harness isn't UI-configurable or the body is invalid, 403 when not the owner, 409 when offline, 502 on host-side failure, 504 on timeout.",
"description": "Write a harness provider credential onto a connected host.\n\nBacks the Web UI setup dialog's \"Add a credential\" action so a user can\nconfigure a Claude / Codex / Pi credential on a connected host without a\nterminal. Owner-scoped, allowlisted, and gated behind\n`harness_install` release feature exactly like the install route\n(404 when disabled). The host daemon does the write with the same\nnon-interactive core the `omnigent setup` wizard uses.\n\nSecurity: the server is an authz'd pass-through \u2014 it validates\nownership + the allowlist and forwards the secret over the (TLS) tunnel;\nit never persists the secret or logs it. The secret rides in the request\nbody (not the URL), and the frame field is redaction-named so it never\nlands on a telemetry span.\n\n**Parameters**\n\n- `body` \u2014 The credential payload (kind + secret / gateway / adopt).\n\n**Returns:** `{\"object\": \"harness_credential\", \"harness\": ..., \"configured_harnesses\": {...}, \"gateway_inference\": {...} | None}` \u2014 refreshed readiness so the UI can flip the badge without a reconnect, plus the refreshed gateway-inference map (`None` when the host didn't report one).\n\n**Raises**\n\n- `HTTPException` \u2014 404 when disabled or host unknown, 400 when the harness isn't UI-configurable or the body is invalid, 403 when not the owner, 409 when offline, 502 on host-side failure, 504 on timeout.",
"operationId": "store_host_harness_credential_v1_hosts__host_id__harnesses__harness__credential_post",
"parameters": [
{
@@ -7757,7 +7757,7 @@
},
"/v1/hosts/{host_id}/harnesses/{harness}/install": {
"post": {
"description": "Install a missing, npm-installable harness CLI onto a host.\n\nBacks the Web UI's New Chat dialog \"Install\" action so a user can\ninstall a harness the connected host is missing without dropping to a\nterminal. Owner-scoped like the other host actions: only the host owner\nmay install onto it. Scoped to the UI-installable allowlist (claude,\ncodex, pi, opencode, qwen) \u2014 curl/brew and interactive-auth harnesses\nare refused. The whole route is gated behind\n`OMNIGENT_HARNESS_INSTALL_ENABLED` (default off): when disabled it\nreturns 404 so the feature is invisible until opted in.\n\nConcurrent requests for the same (host, harness) coalesce onto one\nin-flight install so a double-click can't fire two global npm installs.\n\n**Returns:** `{\"object\": \"harness_install\", \"harness\": ..., \"configured_harnesses\": {...}, \"gateway_inference\": {...} | None}` \u2014 the host's refreshed readiness map so the UI can flip the badge without a reconnect, plus its refreshed gateway-inference map (`None` when the host didn't report one).\n\n**Raises**\n\n- `HTTPException` \u2014 404 when the feature is disabled or the host is unknown, 400 when the harness is not UI-installable, 403 when the caller is not the host owner, 409 when the host is offline, 502 on a host-side install failure, 504 on host timeout.",
"description": "Install a missing, npm-installable harness CLI onto a host.\n\nBacks the Web UI's New Chat dialog \"Install\" action so a user can\ninstall a harness the connected host is missing without dropping to a\nterminal. Owner-scoped like the other host actions: only the host owner\nmay install onto it. Scoped to the UI-installable allowlist (claude,\ncodex, pi, opencode, qwen) \u2014 curl/brew and interactive-auth harnesses\nare refused. The whole route is gated behind\n`harness_install` in `OMNIGENT_FEATURES` (default off): when\ndisabled it returns 404 so the feature is invisible until opted in.\n\nConcurrent requests for the same (host, harness) coalesce onto one\nin-flight install so a double-click can't fire two global npm installs.\n\n**Returns:** `{\"object\": \"harness_install\", \"harness\": ..., \"configured_harnesses\": {...}, \"gateway_inference\": {...} | None}` \u2014 the host's refreshed readiness map so the UI can flip the badge without a reconnect, plus its refreshed gateway-inference map (`None` when the host didn't report one).\n\n**Raises**\n\n- `HTTPException` \u2014 404 when the feature is disabled or the host is unknown, 400 when the harness is not UI-installable, 403 when the caller is not the host owner, 409 when the host is offline, 502 on a host-side install failure, 504 on host timeout.",
"operationId": "install_host_harness_v1_hosts__host_id__harnesses__harness__install_post",
"parameters": [
{
+3
View File
@@ -31,6 +31,9 @@ services:
value: /data/admin-credentials
- key: HOST
value: 0.0.0.0
# Comma-separated deployment-wide release features; empty is all off.
- key: OMNIGENT_FEATURES
value: ""
- key: OMNIGENT_AUTH_PROVIDER
value: accounts
- key: OMNIGENT_ACCOUNTS_AUTO_OPEN
@@ -335,7 +335,7 @@ async def _drive_codex_badge(base_url: str) -> None:
await _open_entry_config(page, "ag_polly_e2e")
badge = page.get_by_test_id("new-chat-landing-harness-warning-codex").first
await expect(badge).to_be_visible(timeout=30_000)
# This test doesn't enable OMNIGENT_HARNESS_INSTALL_ENABLED, so the
# This test doesn't enable harness_install in OMNIGENT_FEATURES, so the
# picker runs on the feature-OFF default — where the badge keeps the
# original per-reason text ("needs auth"). (With the feature ON the
# badge collapses to a single "needs setup" and the reason moves into
@@ -0,0 +1,71 @@
"""Playwright coverage for the deployment-wide Usage page release feature."""
from __future__ import annotations
import json
from playwright.sync_api import Page, Route, expect
def _stub_server_info(page: Page, *, usage_page: bool) -> None:
"""Advertise one deterministic ``usage_page`` feature value."""
body = json.dumps(
{
"accounts_enabled": False,
"single_user": True,
"login_url": None,
"needs_setup": False,
"features": {
"usage_page": usage_page,
"harness_install": False,
},
"harness_install_enabled": False,
"installable_harnesses": [],
}
)
def handle_info(route: Route) -> None:
route.fulfill(status=200, content_type="application/json", body=body)
page.route("**/v1/info", handle_info)
def test_usage_page_route_and_navigation_are_absent_when_feature_is_off(
page: Page,
live_server: str,
) -> None:
"""A direct deep link cannot bypass the default-off navigation gate."""
_stub_server_info(page, usage_page=False)
page.goto(f"{live_server}/usage")
expect(page.get_by_role("heading", name="Page not found")).to_be_visible(timeout=30_000)
expect(page.get_by_test_id("usage-nav")).to_have_count(0)
def test_usage_page_route_and_navigation_are_available_when_feature_is_on(
page: Page,
live_server: str,
) -> None:
"""The advertised feature enables both entry points and report rendering."""
_stub_server_info(page, usage_page=True)
report = json.dumps(
{
"cost_today": 0.0,
"cost_last_7d": 0.0,
"cost_last_30d": 0.0,
"total_cost_usd": 0.0,
"daily_costs": [],
"sessions": [],
}
)
def handle_usage(route: Route) -> None:
route.fulfill(status=200, content_type="application/json", body=report)
page.route("**/v1/usage", handle_usage)
page.goto(f"{live_server}/usage")
expect(page.get_by_test_id("usage-nav")).to_be_visible(timeout=30_000)
expect(page.get_by_role("heading", name="Usage", exact=True)).to_be_visible()
expect(page.get_by_text("$0.00", exact=True)).to_be_visible()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 106 KiB

+23
View File
@@ -207,6 +207,29 @@ def test_ensure_local_omnigent_server_respawns_on_config_drift(
)
def test_server_config_signature_changes_with_features(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Changing the startup feature set forces a managed-server respawn."""
monkeypatch.delenv("OMNIGENT_FEATURES", raising=False)
sig_off = local_server.server_config_signature()
monkeypatch.setenv("OMNIGENT_FEATURES", "usage_page")
sig_on = local_server.server_config_signature()
assert sig_off != sig_on
assert sig_on == local_server.server_config_signature()
def test_remote_daemon_signature_ignores_local_server_features(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Remote host daemons do not parse config for a server they do not own."""
monkeypatch.setenv("OMNIGENT_FEATURES", "not-a-feature")
assert local_server.server_config_signature(include_features=False)
def test_server_config_signature_changes_with_version(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -9,10 +9,10 @@ owner-scoped, host-forwarded design.
These are the executable acceptance criteria for Milestone 1 of the
"Setup From the UI" project: turning the dead-end "binary missing"
warning into a working Install action. The route is gated behind
``OMNIGENT_HARNESS_INSTALL_ENABLED``; the fixture enables it so the
happy-path and validation cases can run, and one test asserts the route
is 404 (invisible) when the flag is off.
warning into a working Install action. The route is gated by
``harness_install`` in ``OMNIGENT_FEATURES``; the fixture enables it so the
happy-path and validation cases can run, and one test asserts the route is 404
(invisible) when the flag is off.
"""
from __future__ import annotations
@@ -35,6 +35,7 @@ from omnigent.host.frames import (
decode_host_frame,
encode_host_frame,
)
from omnigent.server.feature_flags import FeatureFlags
from omnigent.server.host_registry import HostRegistry
from omnigent.server.routes.host_tunnel import create_host_tunnel_router
from omnigent.server.routes.hosts import create_hosts_router
@@ -58,10 +59,10 @@ _HOST_NAME = "install-test-laptop"
def _enable_install_flag(monkeypatch: pytest.MonkeyPatch) -> None:
"""Enable the feature flag for every test except the flag-off case.
The route is invisible (404) unless ``OMNIGENT_HARNESS_INSTALL_ENABLED``
is truthy; the happy-path and validation tests need it on.
The route is invisible (404) unless ``harness_install`` is in the enabled
feature set; the happy-path and validation tests need it on.
"""
monkeypatch.setenv("OMNIGENT_HARNESS_INSTALL_ENABLED", "1")
monkeypatch.setenv("OMNIGENT_FEATURES", "harness_install")
def _websocket_scope(path: str) -> dict[str, object]:
@@ -455,16 +456,24 @@ async def test_install_coalesces_concurrent_same_family(
async def test_install_harness_route_hidden_when_flag_off(
install_app: tuple[FastAPI, HostRegistry, HostStore, SqlAlchemyConversationStore],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
With the flag off the route is 404 — the feature is invisible.
Ships dark by default; only opt-in deployments expose it.
"""
monkeypatch.setenv("OMNIGENT_HARNESS_INSTALL_ENABLED", "0")
app, _reg, _hs, _cs = install_app
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
_app, registry, host_store, conv_store = install_app
off_app = FastAPI()
off_app.include_router(
create_hosts_router(
registry,
host_store,
conv_store,
feature_flags=FeatureFlags(),
),
prefix="/v1",
)
async with AsyncClient(transport=ASGITransport(app=off_app), base_url="http://test") as client:
resp = await client.post(f"/v1/hosts/{_HOST_ID}/harnesses/claude/install")
assert resp.status_code == 404
@@ -34,6 +34,7 @@ from omnigent.host.frames import (
decode_host_frame,
encode_host_frame,
)
from omnigent.server.feature_flags import FeatureFlags
from omnigent.server.host_registry import HostRegistry
from omnigent.server.routes.host_tunnel import create_host_tunnel_router
from omnigent.server.routes.hosts import create_hosts_router
@@ -54,7 +55,7 @@ _HOST_NAME = "credential-test-laptop"
@pytest.fixture(autouse=True)
def _enable_flag(monkeypatch: pytest.MonkeyPatch) -> None:
"""Enable the feature flag for every test except the flag-off case."""
monkeypatch.setenv("OMNIGENT_HARNESS_INSTALL_ENABLED", "1")
monkeypatch.setenv("OMNIGENT_FEATURES", "harness_install")
def _websocket_scope(path: str) -> dict[str, object]:
@@ -395,12 +396,20 @@ async def test_concurrent_writes_to_one_host_are_serialized(
async def test_route_hidden_when_flag_off(
cred_app: tuple[FastAPI, HostRegistry, HostStore, SqlAlchemyConversationStore],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""With the flag off the route is 404 — the feature is invisible."""
monkeypatch.setenv("OMNIGENT_HARNESS_INSTALL_ENABLED", "0")
app, _reg, _hs, _cs = cred_app
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
_app, registry, host_store, conv_store = cred_app
off_app = FastAPI()
off_app.include_router(
create_hosts_router(
registry,
host_store,
conv_store,
feature_flags=FeatureFlags(),
),
prefix="/v1",
)
async with AsyncClient(transport=ASGITransport(app=off_app), base_url="http://test") as client:
resp = await client.post(
f"/v1/hosts/{_HOST_ID}/harnesses/claude/credential",
json={"kind": "key", "secret": "x"},
@@ -548,11 +557,19 @@ async def test_detect_credentials_returns_non_secret_descriptors(
async def test_detect_credentials_hidden_when_flag_off(
cred_app: tuple[FastAPI, HostRegistry, HostStore, SqlAlchemyConversationStore],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""With the flag off the detect route is 404."""
monkeypatch.setenv("OMNIGENT_HARNESS_INSTALL_ENABLED", "0")
app, _reg, _hs, _cs = cred_app
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
_app, registry, host_store, conv_store = cred_app
off_app = FastAPI()
off_app.include_router(
create_hosts_router(
registry,
host_store,
conv_store,
feature_flags=FeatureFlags(),
),
prefix="/v1",
)
async with AsyncClient(transport=ASGITransport(app=off_app), base_url="http://test") as client:
resp = await client.get(f"/v1/hosts/{_HOST_ID}/credentials/detected")
assert resp.status_code == 404
@@ -14,6 +14,9 @@ from types import SimpleNamespace
import httpx
import pytest
from fastapi import FastAPI
from omnigent.server.feature_flags import Feature, FeatureFlags
pytestmark = pytest.mark.asyncio
@@ -82,8 +85,11 @@ async def test_info_returns_expected_fields(client: httpx.AsyncClient) -> None:
assert data["needs_setup"] is False
assert isinstance(data["databricks_features"], bool)
assert isinstance(data["managed_sandboxes_enabled"], bool)
# harness_install_enabled gates the UI Install action; default off unless
# OMNIGENT_HARNESS_INSTALL_ENABLED is set, so it's false in the test app.
assert data["features"] == {
"usage_page": False,
"harness_install": False,
}
# Compatibility field for frontend builds predating the nested map.
assert data["harness_install_enabled"] is False
# installable_harnesses is the allowlist the SPA offers setup for; blank
# while the feature is off so the UI never offers an install the disabled
@@ -117,7 +123,9 @@ async def test_info_single_user_false_without_marker(
async def test_info_advertises_installable_harnesses_when_enabled(
client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
app: FastAPI,
client: httpx.AsyncClient,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""With the feature on, ``/v1/info`` publishes the install allowlist.
@@ -126,12 +134,13 @@ async def test_info_advertises_installable_harnesses_when_enabled(
declares (``codex-native``), not just the bare ids.
"""
from omnigent.onboarding.harness_install import ui_installable_harnesses
from omnigent.server.routes.hosts import HARNESS_INSTALL_ENABLED_ENV
monkeypatch.setenv(HARNESS_INSTALL_ENABLED_ENV, "1")
enabled = FeatureFlags(frozenset({Feature.HARNESS_INSTALL}))
monkeypatch.setattr(app.state, "feature_flags", enabled)
resp = await client.get("/v1/info")
assert resp.status_code == 200
data = resp.json()
assert data["features"]["harness_install"] is True
assert data["harness_install_enabled"] is True
assert set(data["installable_harnesses"]) == set(ui_installable_harnesses())
assert "codex-native" in data["installable_harnesses"]
+61
View File
@@ -113,6 +113,67 @@ def test_build_usage_report_summary_from_daily_rollup(
assert report.cost_last_7d == 3.0 # today + 2026-07-18
assert report.cost_last_30d == 7.0 # + 2026-07-01
assert report.total_cost_usd == 15.0 # + 2026-05-01
# The legacy CLI report remains available while page-only details stay dark.
assert report.daily_costs == []
def test_build_usage_report_includes_page_details_when_enabled(
db_uri: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store = SqlAlchemyConversationStore(db_uri)
store.add_daily_cost(RESERVED_USER_LOCAL, "2026-07-22", 1.25)
session_id = _add_session(
store,
monkeypatch,
ts=1_784_678_400,
cost=1.25,
by_model={"model-a": {"total_cost_usd": 1.25}},
title="page session",
)
monkeypatch.setattr("omnigent.db.utils.now_epoch", lambda: 1_784_678_400)
monkeypatch.setattr(
"omnigent.server.routes.usage._resolve_session_harness",
lambda _conv: "codex-native",
)
monkeypatch.setattr(
"omnigent.server.routes.usage._resolve_llm_model",
lambda _conv: "model-a",
)
report = _build_usage_report(store, None, include_page_details=True)
assert [(item.day, item.cost_usd) for item in report.daily_costs] == [("2026-07-22", 1.25)]
session = next(item for item in report.sessions if item.id == session_id)
assert session.harness == "codex-native"
assert session.llm_model == "model-a"
def test_build_usage_report_skips_page_resolution_while_disabled(
db_uri: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store = SqlAlchemyConversationStore(db_uri)
_add_session(
store,
monkeypatch,
ts=1_700_000_000,
cost=1.0,
by_model={},
title="legacy session",
)
def _unexpected(_conv: object) -> str:
pytest.fail("page-only metadata was resolved while usage_page was off")
monkeypatch.setattr("omnigent.server.routes.usage._resolve_session_harness", _unexpected)
monkeypatch.setattr("omnigent.server.routes.usage._resolve_llm_model", _unexpected)
report = _build_usage_report(store, None)
assert report.daily_costs == []
assert report.sessions[0].harness is None
assert report.sessions[0].llm_model is None
def test_build_usage_report_sessions_detail(
+77
View File
@@ -0,0 +1,77 @@
"""Tests for deployment-wide release feature resolution."""
from __future__ import annotations
import dataclasses
import pytest
from omnigent.server.feature_flags import (
FEATURE_DEFINITIONS,
FEATURES_ENV_VAR,
Feature,
FeatureFlags,
resolve_feature_flags,
)
def test_features_default_off() -> None:
flags = resolve_feature_flags({})
assert flags.enabled_features == frozenset()
assert flags.frontend_dict() == {
"usage_page": False,
"harness_install": False,
}
def test_resolves_comma_separated_enabled_set() -> None:
flags = resolve_feature_flags({FEATURES_ENV_VAR: " usage_page, harness_install,usage_page "})
assert flags.enabled(Feature.USAGE_PAGE)
assert flags.enabled(Feature.HARNESS_INSTALL)
assert flags.enabled_names() == ("harness_install", "usage_page")
def test_empty_entries_are_ignored() -> None:
assert resolve_feature_flags({FEATURES_ENV_VAR: " , , "}) == FeatureFlags()
def test_removed_harness_install_variable_fails_with_migration_hint() -> None:
with pytest.raises(ValueError, match="OMNIGENT_FEATURES=harness_install"):
resolve_feature_flags({"OMNIGENT_HARNESS_INSTALL_ENABLED": "1"})
def test_removed_harness_install_variable_allows_explicit_off() -> None:
flags = resolve_feature_flags({"OMNIGENT_HARNESS_INSTALL_ENABLED": "0"})
assert not flags.enabled(Feature.HARNESS_INSTALL)
def test_unknown_feature_fails_with_known_names() -> None:
with pytest.raises(ValueError) as exc_info:
resolve_feature_flags({FEATURES_ENV_VAR: "usage-pgae"})
message = str(exc_info.value)
assert "usage-pgae" in message
assert "usage_page" in message
assert "harness_install" in message
def test_snapshot_is_immutable_and_does_not_follow_environment_mutation() -> None:
environ = {FEATURES_ENV_VAR: "usage_page"}
flags = resolve_feature_flags(environ)
environ[FEATURES_ENV_VAR] = "harness_install"
assert flags.enabled(Feature.USAGE_PAGE)
assert not flags.enabled(Feature.HARNESS_INSTALL)
with pytest.raises(dataclasses.FrozenInstanceError):
flags.enabled_features = frozenset() # type: ignore[misc]
def test_release_flags_have_lifecycle_metadata_and_default_off() -> None:
assert {definition.feature for definition in FEATURE_DEFINITIONS} == set(Feature)
for definition in FEATURE_DEFINITIONS:
assert definition.owner
assert definition.review_by_release
assert not FeatureFlags().enabled(definition.feature)
+48
View File
@@ -0,0 +1,48 @@
import { render, screen } from "@testing-library/react";
import { Outlet, MemoryRouter } from "react-router-dom";
import { describe, expect, it, vi } from "vitest";
import { FALLBACK_SERVER_INFO } from "@/lib/capabilities";
import { CapabilitiesProvider } from "@/lib/CapabilitiesContext";
vi.mock("@/lib/analytics", () => ({ useOmnigentPageView: vi.fn() }));
vi.mock("@/shell/AppShell", () => ({
AppShell: () => (
<div>
<span>app shell</span>
<Outlet />
</div>
),
}));
vi.mock("@/pages/ChatPage", () => ({ ChatPage: () => <div>chat page</div> }));
vi.mock("@/pages/NotFoundPage", () => ({ NotFoundPage: () => <div>not found</div> }));
vi.mock("@/pages/UsagePage", () => ({ UsagePage: () => <div>usage page</div> }));
import App from "./App";
function renderUsageRoute(enabled: boolean) {
const info: typeof FALLBACK_SERVER_INFO = {
...FALLBACK_SERVER_INFO,
features: enabled ? { usage_page: true } : {},
};
return render(
<CapabilitiesProvider info={info}>
<MemoryRouter initialEntries={["/usage"]}>
<App />
</MemoryRouter>
</CapabilitiesProvider>,
);
}
describe("Usage release feature route", () => {
it("does not register /usage while the feature is off", async () => {
renderUsageRoute(false);
expect(await screen.findByText("not found")).toBeInTheDocument();
expect(screen.queryByText("usage page")).toBeNull();
});
it("registers /usage while the feature is on", async () => {
renderUsageRoute(true);
expect(await screen.findByText("usage page")).toBeInTheDocument();
expect(screen.queryByText("not found")).toBeNull();
});
});
+4 -1
View File
@@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from "react-router-dom";
import { ChatPage as ChatPageImpl } from "@/pages/ChatPage";
import { NotFoundPage as NotFoundPageImpl } from "@/pages/NotFoundPage";
import { useOmnigentPageView } from "@/lib/analytics";
import { isFeatureEnabled } from "@/lib/capabilities";
import { useServerInfo } from "@/lib/CapabilitiesContext";
import { AppShell } from "@/shell/AppShell";
@@ -153,7 +154,9 @@ function App({ basename }: AppProps = {}) {
<Route path={`${prefix}/c/:conversationId`} element={<ChatPage />} />
<Route path={`${prefix}/inbox`} element={<InboxPage />} />
<Route path={`${prefix}/tasks`} element={<TasksPage />} />
<Route path={`${prefix}/usage`} element={<UsagePage />} />
{isFeatureEnabled(info, "usage_page") && (
<Route path={`${prefix}/usage`} element={<UsagePage />} />
)}
{/* Settings renders into the chat outlet so the conversations
sidebar stays put — entering settings only swaps the card's
content (the section nav) and the main area. The active section
@@ -274,6 +274,7 @@ const DICTATION_INFO: ServerInfo = {
server_version: "test",
smart_routing_enabled: false,
smart_routing_sources: { external: false, oss: false },
features: {},
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: true,
@@ -69,6 +69,7 @@ function serverInfo(overrides: Partial<ServerInfo> = {}): ServerInfo {
server_version: null,
smart_routing_enabled: false,
smart_routing_sources: { external: false, oss: false },
features: {},
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: false,
+2 -22
View File
@@ -31,7 +31,7 @@ import { TooltipProvider } from "./components/ui/tooltip";
import { ImageLightboxProvider } from "./components/ImageLightbox";
import { RunnerHealthProvider } from "./hooks/RunnerHealthProvider";
import { CapabilitiesContext } from "./lib/CapabilitiesContext";
import { resolveServerInfo, type ServerInfo } from "./lib/capabilities";
import { FALLBACK_SERVER_INFO, resolveServerInfo, type ServerInfo } from "./lib/capabilities";
import { EmbeddedProvider } from "./lib/embedded";
import { type OmnigentHostConfig, setEmbedRoot, setOmnigentHostConfig } from "./lib/host";
import { resolveIdentity } from "./lib/identity";
@@ -100,26 +100,6 @@ export interface OmnigentAppProps extends OmnigentHostConfig {
* as the Radix portal root, so the host only renders this — no class/portal
* wiring needed.
*/
// Sentinel used when the `/v1/info` probe is slow or missing — matches
// `main.tsx`'s fallback (accounts off, no login).
const SERVER_INFO_OFFLINE_FALLBACK: ServerInfo = {
accounts_enabled: false,
single_user: false,
login_url: null,
needs_setup: false,
databricks_features: false,
managed_sandboxes_enabled: false,
sandbox_provider: null,
sharing_mode: "on",
public_sharing_enabled: true,
server_version: null,
smart_routing_enabled: false,
smart_routing_sources: { external: false, oss: false },
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: false,
};
/**
* Runs `main.tsx`'s boot-time `/v1/info` probe inside the embed tree.
*
@@ -146,7 +126,7 @@ function EmbedCapabilitiesProvider({ children }: { children: ReactNode }) {
// full re-navigation. resolveServerInfo never rejects (its failure path
// resolves to the OFF sentinel), so the real value always arrives.
const fallbackTimer = setTimeout(() => {
if (alive && !resolved) setInfo(SERVER_INFO_OFFLINE_FALLBACK);
if (alive && !resolved) setInfo(FALLBACK_SERVER_INFO);
}, 1500);
void resolveServerInfo().then((real) => {
resolved = true;
+31
View File
@@ -26,6 +26,7 @@ function info(overrides: Partial<ServerInfo>): ServerInfo {
server_version: null,
smart_routing_enabled: false,
smart_routing_sources: { external: false, oss: false },
features: {},
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: false,
@@ -119,6 +120,36 @@ describe("resolveServerInfo sandbox_providers", () => {
});
});
describe("resolveServerInfo release features", () => {
it("keeps boolean feature values and drops malformed entries", async () => {
const parsed = await probe({
features: { usage_page: true, harness_install: false, malformed: "yes" },
});
expect(parsed.features).toEqual({ usage_page: true, harness_install: false });
});
it("defaults missing features off", async () => {
const { isFeatureEnabled } = await import("./capabilities");
const parsed = await probe({});
expect(isFeatureEnabled(parsed, "usage_page")).toBe(false);
expect(isFeatureEnabled(parsed, "harness_install")).toBe(false);
});
it("falls back to the legacy harness field from an older server", async () => {
const { isFeatureEnabled } = await import("./capabilities");
const parsed = await probe({ harness_install_enabled: true });
expect(isFeatureEnabled(parsed, "harness_install")).toBe(true);
});
it("fails all release features closed when the probe fails", async () => {
fetchMock.mockRejectedValueOnce(new Error("offline"));
const { isFeatureEnabled, resolveServerInfo } = await import("./capabilities");
const parsed = await resolveServerInfo();
expect(isFeatureEnabled(parsed, "usage_page")).toBe(false);
expect(isFeatureEnabled(parsed, "harness_install")).toBe(false);
});
});
describe("resolveServerInfo smart_routing_sources", () => {
it("reads an explicit field verbatim", async () => {
const parsed = await probe({
+40 -4
View File
@@ -45,6 +45,12 @@ export interface SmartRoutingSources {
oss: boolean;
}
/** Release features understood by this frontend build. */
export type FeatureKey = "usage_page" | "harness_install";
/** Deployment-wide release-feature values advertised by the server. */
export type FeatureValues = Record<string, boolean>;
/** Shape of the response from ``GET /v1/info``. */
export interface ServerInfo {
accounts_enabled: boolean;
@@ -132,8 +138,16 @@ export interface ServerInfo {
*/
smart_routing_sources: SmartRoutingSources;
/**
* Deployment-wide release features. Missing keys are disabled. The map is
* the canonical gate for new frontend surfaces.
*/
features: FeatureValues;
/**
* Compatibility field for servers/frontends predating ``features``.
* New consumers should use :func:`isFeatureEnabled`.
*
* True when the server accepts UI-driven harness installs
* (``OMNIGENT_HARNESS_INSTALL_ENABLED=1``). Gates the New Chat dialog's
* (``harness_install`` in ``OMNIGENT_FEATURES``). Gates the New Chat dialog's
* one-click "Install" action for a missing harness. Fails to ``false`` so a
* failed probe never offers an install the server would reject.
*/
@@ -156,8 +170,8 @@ export interface ServerInfo {
dictation_available: boolean;
}
/** Sentinel used when the probe fails — accounts is off, no login URL. */
const FALLBACK_SERVER_INFO: ServerInfo = {
/** Sentinel used when the probe fails — accounts and release features are off. */
export const FALLBACK_SERVER_INFO: ServerInfo = {
accounts_enabled: false,
// Fail to multi-user: a failed probe must not hide account/sharing chrome.
single_user: false,
@@ -174,6 +188,7 @@ const FALLBACK_SERVER_INFO: ServerInfo = {
server_version: null,
smart_routing_enabled: false,
smart_routing_sources: { external: false, oss: false },
features: {},
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: false,
@@ -194,6 +209,25 @@ function parseSmartRoutingSources(raw: unknown, routingEnabled: boolean): SmartR
return { external: sources.external === true, oss: sources.oss === true };
}
function parseFeatures(raw: unknown, harnessInstallEnabled: boolean): FeatureValues {
const parsed: FeatureValues = {};
if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
for (const [key, value] of Object.entries(raw)) {
if (typeof value === "boolean") parsed[key] = value;
}
}
// A server predating the feature map exposed this one release gate as a
// top-level field. Preserve mixed-version behavior without making it a
// second source for new features.
if (!("harness_install" in parsed)) parsed.harness_install = harnessInstallEnabled;
return parsed;
}
/** Return whether a known release feature is enabled; missing/loading is off. */
export function isFeatureEnabled(info: ServerInfo | "loading", feature: FeatureKey): boolean {
return info !== "loading" && info.features?.[feature] === true;
}
let cachedServerInfo: ServerInfo | null = null;
let pendingServerInfo: Promise<ServerInfo> | null = null;
@@ -217,6 +251,7 @@ export async function resolveServerInfo(): Promise<ServerInfo> {
if (res.ok) {
const data = (await res.json()) as Partial<ServerInfo>;
const smartRoutingEnabled = data.smart_routing_enabled === true;
const harnessInstallEnabled = data.harness_install_enabled === true;
cachedServerInfo = {
accounts_enabled: data.accounts_enabled === true,
single_user: data.single_user === true,
@@ -240,7 +275,8 @@ export async function resolveServerInfo(): Promise<ServerInfo> {
data.smart_routing_sources,
smartRoutingEnabled,
),
harness_install_enabled: data.harness_install_enabled === true,
features: parseFeatures(data.features, harnessInstallEnabled),
harness_install_enabled: harnessInstallEnabled,
installable_harnesses: Array.isArray(data.installable_harnesses)
? data.installable_harnesses.filter((h): h is string => typeof h === "string")
: [],
+1
View File
@@ -25,6 +25,7 @@ const hostWith = (configured: Record<string, boolean | string> | null | undefine
const info = (overrides: Partial<ServerInfo> = {}): ServerInfo =>
({
harness_install_enabled: true,
features: { harness_install: overrides.harness_install_enabled ?? true },
installable_harnesses: ["codex", "codex-native", "pi", "pi-native"],
...overrides,
}) as ServerInfo;
+3 -3
View File
@@ -10,7 +10,7 @@
import type { SetupStepWire } from "@/lib/agentLabels";
import type { Host } from "@/hooks/useHosts";
import type { ServerInfo } from "@/lib/capabilities";
import { isFeatureEnabled, type ServerInfo } from "@/lib/capabilities";
/** Whether a step is satisfied, still needed, or not locally determinable. */
export type SetupStepStatus = "done" | "todo" | "unknown";
@@ -124,7 +124,7 @@ export function harnessInstallableOnHost(
): boolean {
return (
info !== "loading" &&
info.harness_install_enabled &&
isFeatureEnabled(info, "harness_install") &&
!!harness &&
info.installable_harnesses.includes(harness) &&
host?.status === "online"
@@ -182,7 +182,7 @@ export function harnessAuthableOnHost(
): boolean {
return (
info !== "loading" &&
info.harness_install_enabled &&
isFeatureEnabled(info, "harness_install") &&
harnessCredentialFamily(harness) !== null &&
host?.status === "online"
);
+2 -22
View File
@@ -9,7 +9,7 @@ import { ImageLightboxProvider } from "./components/ImageLightbox";
import { RunnerHealthProvider } from "./hooks/RunnerHealthProvider";
import { QueueFlushProvider } from "./hooks/QueueFlushProvider";
import { SessionUpdatesProvider } from "./hooks/SessionUpdatesProvider";
import { resolveServerInfo, type ServerInfo } from "./lib/capabilities";
import { FALLBACK_SERVER_INFO, resolveServerInfo, type ServerInfo } from "./lib/capabilities";
import { CapabilitiesProvider } from "./lib/CapabilitiesContext";
import { resolveIdentity } from "./lib/identity";
import { initNativeInsets } from "./lib/nativeInsets";
@@ -86,27 +86,7 @@ applyThemePalette(readThemePalette());
const bootProbe: Promise<ServerInfo> = Promise.race([
resolveServerInfo(),
new Promise<ServerInfo>((resolve) => {
setTimeout(
() =>
resolve({
accounts_enabled: false,
single_user: false,
login_url: null,
needs_setup: false,
databricks_features: false,
managed_sandboxes_enabled: false,
sandbox_provider: null,
sharing_mode: "on",
public_sharing_enabled: true,
server_version: null,
smart_routing_enabled: false,
smart_routing_sources: { external: false, oss: false },
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: false,
}),
1500,
);
setTimeout(() => resolve(FALLBACK_SERVER_INFO), 1500);
}),
]);
+1
View File
@@ -1644,6 +1644,7 @@ describe("routing eligibility gates", () => {
server_version: null,
smart_routing_enabled: smartRouting,
smart_routing_sources: { external: smartRouting, oss: smartRouting },
features: {},
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: false,
+1
View File
@@ -357,6 +357,7 @@ function serverInfo(overrides: Partial<ServerInfo> = {}): ServerInfo {
server_version: null,
smart_routing_enabled: false,
smart_routing_sources: { external: false, oss: false },
features: {},
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: false,
+1
View File
@@ -760,6 +760,7 @@ function renderLanding(infoOverrides: Partial<ServerInfo> = {}, route = "/") {
// routing). Cases that exercise the built-in judge pass the field
// explicitly.
smart_routing_sources: { external: infoOverrides.smart_routing_enabled === true, oss: false },
features: { harness_install: infoOverrides.harness_install_enabled === true },
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: false,
+4 -4
View File
@@ -83,7 +83,7 @@ import {
// Re-exported for tests that import the readiness helpers from this module.
export { harnessUnavailableReasonOnHost, harnessUnconfiguredOnHost, harnessWarningBadgeText };
import { sandboxOptionLabel, sandboxProviderOptions } from "@/lib/capabilities";
import { isFeatureEnabled, sandboxOptionLabel, sandboxProviderOptions } from "@/lib/capabilities";
import {
isSlashCommandText,
rankedSlashCommandNames,
@@ -1006,7 +1006,7 @@ export function AgentHarnessPicker({
const queryClient = useQueryClient();
const info = useServerInfo();
// Feature ON → single "needs setup" badge; OFF → per-reason original text.
const collapsedBadge = info !== "loading" && info.harness_install_enabled;
const collapsedBadge = isFeatureEnabled(info, "harness_install");
const triggerRef = useRef<HTMLButtonElement>(null);
// Touch devices can't hover, so the desktop submenu flyouts ("More",
@@ -1436,7 +1436,7 @@ function HarnessConfigModal({
}) {
const info = useServerInfo();
// Feature ON → single "needs setup" badge; OFF → per-reason original text.
const collapsedBadge = info !== "loading" && info.harness_install_enabled;
const collapsedBadge = isFeatureEnabled(info, "harness_install");
const entryHarness = nativeCodingAgentForAvailableAgent(agent)?.harness ?? null;
const hasPermission = nativeAgentHasCapability(agent, "permissionMode");
const hasApproval = nativeAgentHasCapability(agent, "approvalMode");
@@ -2015,7 +2015,7 @@ export function NewChatLandingScreen() {
// Gates the whole UI-driven setup experience (Set up affordance + dialog +
// collapsed badge). OFF → the composer/picker fall back to the original
// "run omni setup" guidance, so a disabled flag is a no-op on the UI.
const harnessInstallEnabled = info !== "loading" && info.harness_install_enabled;
const harnessInstallEnabled = isFeatureEnabled(info, "harness_install");
// Unfiltered brain-harness labels: safe for membership checks and for
// labelling an existing pick, but the OPTIONS offered in the gear modal use
// the gated `brainHarnessLabels` below, which drops the fully-auto row when
@@ -206,6 +206,7 @@ function serverInfo(overrides: Partial<ServerInfo> = {}): ServerInfo {
server_version: null,
smart_routing_enabled: false,
smart_routing_sources: { external: false, oss: false },
features: {},
harness_install_enabled: false,
installable_harnesses: [],
dictation_available: false,
+29 -2
View File
@@ -12,6 +12,8 @@ import { MemoryRouter, Route, Routes } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TooltipProvider } from "@/components/ui/tooltip";
import type { Conversation } from "@/hooks/useConversations";
import { FALLBACK_SERVER_INFO, type ServerInfo } from "@/lib/capabilities";
import { CapabilitiesProvider } from "@/lib/CapabilitiesContext";
// Project mocks are declared via vi.hoisted so they exist before the hoisted
// vi.mock factory runs. projectsMock is mutated per-test to drive project
@@ -211,13 +213,19 @@ function mockConversations(convs: Conversation[]) {
useConvMock.mockImplementation(() => result(convs));
}
function renderSidebar(open = true, initialEntry = "/", onOpenSearch?: () => void) {
function renderSidebar(
open = true,
initialEntry = "/",
onOpenSearch?: () => void,
info?: ServerInfo,
) {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const sidebar = <Sidebar open={open} onClose={vi.fn()} onOpenSearch={onOpenSearch} />;
return render(
<QueryClientProvider client={qc}>
<TooltipProvider>
<MemoryRouter initialEntries={[initialEntry]}>
<Sidebar open={open} onClose={vi.fn()} onOpenSearch={onOpenSearch} />
{info ? <CapabilitiesProvider info={info}>{sidebar}</CapabilitiesProvider> : sidebar}
</MemoryRouter>
</TooltipProvider>
</QueryClientProvider>,
@@ -631,6 +639,25 @@ describe("Sidebar session list", () => {
expect(badge).not.toHaveClass("bg-[var(--sidebar-active)]");
});
it("hides Usage navigation while the release feature is off", () => {
mockConversations(THREE_TYPE_CONVERSATIONS);
renderSidebar();
expect(screen.queryByTestId("usage-nav")).toBeNull();
});
it("shows and highlights Usage navigation when the release feature is on", () => {
mockConversations(THREE_TYPE_CONVERSATIONS);
renderSidebar(true, "/usage", undefined, {
...FALLBACK_SERVER_INFO,
features: { usage_page: true },
});
const usage = screen.getByTestId("usage-nav");
expect(usage).toHaveAttribute("href", "/usage");
expect(usage).toHaveClass("bg-[var(--sidebar-active)]");
});
it("keeps filtering visible while session selection remains hover-revealed", () => {
mockConversations(THREE_TYPE_CONVERSATIONS);
renderSidebar();
+28 -24
View File
@@ -127,7 +127,7 @@ import {
import { useHosts, type Host } from "@/hooks/useHosts";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useServerInfo } from "@/lib/CapabilitiesContext";
import { isSingleUserMode, sandboxOptionLabel } from "@/lib/capabilities";
import { isFeatureEnabled, isSingleUserMode, sandboxOptionLabel } from "@/lib/capabilities";
import { relativeTime } from "@/lib/relativeTime";
import { showToast } from "@/components/ui/toast";
import { PermissionsModal } from "@/components/PermissionsModal";
@@ -473,6 +473,8 @@ export function Sidebar({
onOpenSearch,
peek,
}: SidebarProps) {
const serverInfo = useServerInfo();
const usagePageEnabled = isFeatureEnabled(serverInfo, "usage_page");
const [selectionMode, setSelectionMode] = useState(false);
// Which rows the current selection targets: the flat "Sessions" list, or the
// sessions nested inside project folders. Set when selection mode is entered
@@ -923,29 +925,31 @@ export function Sidebar({
)}
</Link>
</Button>
<Button
asChild
variant="ghost"
className={cn(
SIDEBAR_ROW,
"w-full justify-start border-0 font-normal",
SIDEBAR_HOVER_HIGHLIGHT,
isUsagePage && SIDEBAR_ACTIVE_HIGHLIGHT,
)}
data-testid="usage-nav"
>
<Link to="/usage" onClick={onNavClick}>
<WalletIcon
className={cn(
"ui-icon",
isUsagePage
? "text-[var(--sidebar-active-foreground)]"
: "text-muted-foreground",
)}
/>
Usage
</Link>
</Button>
{usagePageEnabled && (
<Button
asChild
variant="ghost"
className={cn(
SIDEBAR_ROW,
"w-full justify-start border-0 font-normal",
SIDEBAR_HOVER_HIGHLIGHT,
isUsagePage && SIDEBAR_ACTIVE_HIGHLIGHT,
)}
data-testid="usage-nav"
>
<Link to="/usage" onClick={onNavClick}>
<WalletIcon
className={cn(
"ui-icon",
isUsagePage
? "text-[var(--sidebar-active-foreground)]"
: "text-muted-foreground",
)}
/>
Usage
</Link>
</Button>
)}
</div>
<nav