diff --git a/components/backend/docs/adr/README.md b/components/backend/docs/adr/README.md deleted file mode 100644 index c5fce6e76..000000000 --- a/components/backend/docs/adr/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Architectural Decision Records (ADRs) - -This directory contains Architectural Decision Records (ADRs) documenting significant architectural decisions made for the Ambient Code Platform. - -## What is an ADR? - -An ADR captures: - -- **Context:** What problem were we solving? -- **Options:** What alternatives did we consider? -- **Decision:** What did we choose and why? -- **Consequences:** What are the trade-offs? - -ADRs are immutable once accepted. If a decision changes, we create a new ADR that supersedes the old one. - -## When to Create an ADR - -Create an ADR for decisions that: - -- Affect the overall architecture -- Are difficult or expensive to reverse -- Impact multiple components or teams -- Involve significant trade-offs -- Will be questioned in the future ("Why did we do it this way?") - -**Examples:** - -- Choosing a programming language or framework -- Selecting a database or messaging system -- Defining authentication/authorization approach -- Establishing API design patterns -- Multi-tenancy architecture decisions - -**Not ADR-worthy:** - -- Trivial implementation choices -- Decisions easily reversed -- Component-internal decisions with no external impact - -## ADR Workflow - -1. **Propose:** Copy `template.md` to `NNNN-title.md` with status "Proposed" -2. **Discuss:** Share with team, gather feedback -3. **Decide:** Update status to "Accepted" or "Rejected" -4. **Implement:** Reference ADR in PRs -5. **Learn:** Update "Implementation Notes" with gotchas discovered - -## ADR Status Meanings - -- **Proposed:** Decision being considered, open for discussion -- **Accepted:** Decision made and being implemented -- **Deprecated:** Decision no longer relevant but kept for historical context -- **Superseded by ADR-XXXX:** Decision replaced by a newer ADR - -## Current ADRs - -| ADR | Title | Status | Date | -|-----|-------|--------|------| -| [0001](0001-kubernetes-native-architecture.md) | Kubernetes-Native Architecture | Accepted | 2024-11-21 | -| [0002](0002-user-token-authentication.md) | User Token Authentication for API Operations | Accepted | 2024-11-21 | -| [0003](0003-multi-repo-support.md) | Multi-Repository Support in AgenticSessions | Accepted | 2024-11-21 | -| [0004](0004-go-backend-python-runner.md) | Go Backend with Python Claude Runner | Accepted | 2024-11-21 | -| [0005](0005-nextjs-shadcn-react-query.md) | Next.js with Shadcn UI and React Query | Accepted | 2024-11-21 | -| [0006](0006-unleash-feature-flags.md) | Unleash for Feature Flag Management | Accepted | 2026-02-17 | - -## References - -- [ADR GitHub Organization](https://adr.github.io/) - ADR best practices -- [Documenting Architecture Decisions](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) - Original proposal by Michael Nygard diff --git a/components/runners/ambient-runner/README.md b/components/runners/ambient-runner/README.md index a75c8284a..e12673035 100644 --- a/components/runners/ambient-runner/README.md +++ b/components/runners/ambient-runner/README.md @@ -4,7 +4,7 @@ The Claude Code Runner is a Python-based component that wraps the Claude Code SD ## Architecture -The runner follows the [Ambient Runner SDK architecture (ADR-0006)](../../docs/adr/0006-ambient-runner-sdk-architecture.md) with a layered design: +The runner follows the [Ambient Runner SDK architecture (ADR-0006)](../../../docs/internal/adr/0006-ambient-runner-sdk-architecture.md) with a layered design: ### Core Modules diff --git a/docs/internal/README.md b/docs/internal/README.md index 0cb20ad55..c4d7ad526 100644 --- a/docs/internal/README.md +++ b/docs/internal/README.md @@ -6,7 +6,7 @@ This directory contains internal developer documentation for the Ambient Code Pl | Directory | Description | |-----------|-------------| -| `adr/` | Architectural Decision Records (ADR-0001 through ADR-0006) | +| `adr/` | Architectural Decision Records (ADR-0001 through ADR-0007) | | `agents/` | AI agent persona definitions used in workflows | | `api/` | Internal API reference notes (GitLab endpoints) | | `architecture/` | Architecture overviews, Mermaid diagrams, and screenshots | diff --git a/components/backend/docs/adr/0006-unleash-feature-flags.md b/docs/internal/adr/0007-unleash-feature-flags.md similarity index 81% rename from components/backend/docs/adr/0006-unleash-feature-flags.md rename to docs/internal/adr/0007-unleash-feature-flags.md index 3ca54e4f2..362a118f6 100644 --- a/components/backend/docs/adr/0006-unleash-feature-flags.md +++ b/docs/internal/adr/0007-unleash-feature-flags.md @@ -1,13 +1,13 @@ -# ADR-0006: Unleash for Feature Flag Management +# ADR-0007: Unleash for Feature Flag Management -**Status:** Proposed +**Status:** Accepted **Date:** 2026-02-17 **Deciders:** Platform Team -**Technical Story:** Constitution Principle XI requires all new features gated behind feature flags +**Technical Story:** All new features should be gated behind feature flags ## Context and Problem Statement -The project constitution (Principle XI: Feature Flag Discipline) mandates that all new features must be gated behind feature flags. We need a feature flag system that supports: +All new features should be gated behind feature flags. We need a feature flag system that supports: 1. Gradual rollouts and percentage-based targeting 2. A/B testing and experimentation @@ -20,7 +20,7 @@ How should we implement feature flag management for the platform? ## Decision Drivers -* **Constitution compliance:** Principle XI requires feature flags for all new features +* **Feature flag discipline:** All new features should be gated behind feature flags * **Workspace autonomy:** Workspace admins need to control features for their workspace * **Platform control:** Platform team needs gradual rollout and A/B testing capabilities * **Operational control:** Need to enable/disable features without redeployment @@ -173,6 +173,43 @@ Chosen option: **"Tag-based filtering"**, because: | (not set) | disabled | `false` | Platform team | | (not set) | 50% rollout | (evaluated) | Platform team | +### Fail Modes + +The backend provides two categories of flag evaluation functions with different default behaviors when Unleash is unavailable or not configured: + +| Function | Fail Mode | Default | Rationale | +|----------|-----------|---------|-----------| +| `IsEnabled()` | Fail-closed | `false` | General features should be off until explicitly enabled | +| `IsEnabledWithContext()` | Fail-closed | `false` | Same as above, with user/session/IP context | +| `IsModelEnabled()` | Fail-open | `true` | Models should remain available; flags only restrict | +| `IsModelEnabledWithContext()` | Fail-open | `true` | Same as above, with user/session/IP context | + +**Handler-level wrappers** (in `handlers/featureflags.go`): +- `FeatureEnabled(flagName)` — calls `IsEnabled()`, fail-closed +- `FeatureEnabledForRequest(c, flagName)` — calls `IsEnabledWithContext()`, fail-closed + +**Workspace-aware wrappers** (check ConfigMap override first, then Unleash): +- `isModelEnabledWithOverrides(flagName, overrides)` — falls back to `IsModelEnabled()`, fail-open +- `isRunnerEnabledWithOverrides(flagName, overrides)` — falls back to `FeatureEnabled()`, fail-closed + +See [fail-modes.md](../feature-flags/fail-modes.md) for a full reference. + +### Model and Runner Feature Gates + +**Model flags** follow the naming pattern `model..enabled` and are auto-generated from `models.json` at startup. Models marked `featureGated: true` require an enabled flag to appear in the model list. Default models (global and per-provider) bypass flag checks entirely. + +**Runner flags** follow the naming pattern `runner..enabled` and are defined in `flags.json` or via the `featureGate` field in the agent registry. Runners with an empty `featureGate` are always enabled. The default runner (`claude-code`) fails open when the agent registry is unavailable. + +### Flag Sync at Startup + +The backend syncs flag definitions to Unleash on startup (`cmd/sync_flags.go`): + +1. **Model flags** — Generated from `models.json` for available, feature-gated, non-default models. Created with `EnabledByDefault: true` (100% rollout, enabled in environment). Tagged `scope:workspace`. +2. **Generic flags** — Loaded from `flags.json`. Created with `EnabledByDefault: false` (0% rollout) unless specified. +3. **Stale cleanup** — Flags for models no longer feature-gated are archived in Unleash. + +Sync runs asynchronously with 3 retries (10s delay). Requires `UNLEASH_ADMIN_URL` and `UNLEASH_ADMIN_TOKEN`; skips silently if not set. + ### Use Cases | Scenario | Implementation | @@ -201,7 +238,7 @@ data: frontend.new-chat-ui.enabled: "false" ``` -### Flag Naming Convention (Constitution Principle XI) +### Flag Naming Convention All feature flags MUST follow the naming pattern: @@ -260,27 +297,39 @@ func isWorkspaceConfigurable(tags []Tag) bool { | Endpoint | Method | Description | |----------|--------|-------------| -| `/projects/:name/feature-flags` | GET | List all flags with override status | -| `/projects/:name/feature-flags/evaluate/:flagName` | GET | Evaluate flag for workspace | -| `/projects/:name/feature-flags/:flagName/override` | PUT | Set workspace override | -| `/projects/:name/feature-flags/:flagName/override` | DELETE | Remove workspace override | +| `/projects/:name/feature-flags` | GET | List all workspace-configurable flags with override status | +| `/projects/:name/feature-flags/evaluate/:flagName` | GET | Evaluate flag for workspace (ConfigMap then Unleash) | +| `/projects/:name/feature-flags/:flagName` | GET | Get single flag details from Unleash | +| `/projects/:name/feature-flags/:flagName/override` | PUT | Set workspace override (`{"enabled": bool}`) | +| `/projects/:name/feature-flags/:flagName/override` | DELETE | Remove workspace override (revert to Unleash) | +| `/projects/:name/feature-flags/:flagName/enable` | POST | Enable flag (sets ConfigMap override to `"true"`) | +| `/projects/:name/feature-flags/:flagName/disable` | POST | Disable flag (sets ConfigMap override to `"false"`) | ### Key Files **Backend:** -* `handlers/featureflags.go` - Client API proxy for frontend SDK -* `handlers/featureflags_admin.go` - Flag evaluation, override management +* `featureflags/featureflags.go` - Unleash SDK init, `IsEnabled`, `IsModelEnabled` (fail-open/closed defaults) +* `handlers/featureflags.go` - Handler-level wrappers (`FeatureEnabled`, `FeatureEnabledForRequest`) +* `handlers/featureflags_admin.go` - Flag evaluation, override management, workspace CRUD +* `handlers/models.go` - Model listing with feature gate checks (`isModelEnabledWithOverrides`) +* `handlers/runner_types.go` - Runner listing with feature gate checks (`isRunnerEnabledWithOverrides`) +* `cmd/sync_flags.go` - Flag sync to Unleash at startup (models.json + flags.json) * `routes.go` - Route registration **Frontend:** * `src/lib/feature-flags.ts` - Re-exports Unleash SDK hooks (`useFlag`, `useVariant`) * `src/components/providers/feature-flag-provider.tsx` - Unleash provider with environment context -* `src/components/workspace-sections/feature-flags-settings.tsx` - Admin UI (in Settings tab) with batch save +* `src/components/workspace-sections/feature-flags-section.tsx` - Admin UI with batch save * `src/services/queries/use-feature-flags-admin.ts` - React Query hooks including `useWorkspaceFlag` * `src/services/api/feature-flags-admin.ts` - API service functions -* `src/app/api/projects/[name]/feature-flags/*` - Next.js proxy routes +* `src/app/api/feature-flags/route.ts` - Next.js proxy to Unleash Frontend API + +**Configuration:** + +* `manifests/base/models.json` - Model manifest with `featureGated` flags +* `manifests/base/flags.json` - Generic feature flag definitions **Deployment:** @@ -437,5 +486,5 @@ make unleash-port-forward * [Unleash React SDK](https://docs.getunleash.io/reference/sdks/react) * [Unleash Admin API](https://docs.getunleash.io/reference/api/unleash/admin) * [Kubernetes ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/) -* Constitution Principle XI: Feature Flag Discipline -* Related: docs/feature-flags/README.md +* Feature Flag Discipline: All new features gated behind feature flags +* Related: [Feature Flags Documentation](../feature-flags/README.md) diff --git a/docs/internal/adr/README.md b/docs/internal/adr/README.md index 6360d0e1f..9342ea71c 100644 --- a/docs/internal/adr/README.md +++ b/docs/internal/adr/README.md @@ -61,6 +61,8 @@ Create an ADR for decisions that: | [0003](0003-multi-repo-support.md) | Multi-Repository Support in AgenticSessions | Accepted | 2024-11-21 | | [0004](0004-go-backend-python-runner.md) | Go Backend with Python Claude Runner | Accepted | 2024-11-21 | | [0005](0005-nextjs-shadcn-react-query.md) | Next.js with Shadcn UI and React Query | Accepted | 2024-11-21 | +| [0006](0006-ambient-runner-sdk-architecture.md) | Ambient Runner SDK Architecture | Accepted | 2024-11-21 | +| [0007](0007-unleash-feature-flags.md) | Unleash for Feature Flag Management | Accepted | 2026-02-17 | ## References diff --git a/docs/internal/feature-flags/README.md b/docs/internal/feature-flags/README.md index 8210c9127..60cbc9fba 100644 --- a/docs/internal/feature-flags/README.md +++ b/docs/internal/feature-flags/README.md @@ -12,7 +12,8 @@ Use [Unleash](https://www.getunleash.io/) to enable or disable features without - **Frontend**: Next.js proxy at `/api/feature-flags`; use `useFlag()` / `useVariant()` from `@/lib/feature-flags` in client components - **Backend**: Go SDK; use `handlers.FeatureEnabled()` or `handlers.FeatureEnabledForRequest()` in handlers - **Admin UI**: Manage feature toggles directly from the workspace UI (see below) -- When Unleash is not configured, all flags are disabled (safe default) +- **Workspace overrides**: ConfigMap-based per-workspace overrides take precedence over Unleash global state +- When Unleash is not configured, general flags are disabled (fail-closed) and model flags remain enabled (fail-open). See [fail-modes.md](fail-modes.md) for details. **Environment variables:** `UNLEASH_URL`, `UNLEASH_CLIENT_KEY` (and optionally `UNLEASH_APP_NAME` for frontend). See the guide for per-component details. @@ -81,6 +82,11 @@ if handlers.FeatureEnabled("my-feature") { --- +## Available Documentation + +- **[Unleash Integration Guide](feature-flags-unleash.md)** — Setup, usage in frontend/backend, admin UI, API endpoints +- **[Fail Modes Reference](fail-modes.md)** — Which methods fail open vs closed, where each is used, evaluation precedence + ## Related Documentation - [Frontend README](../../components/frontend/README.md) – Development and env overview diff --git a/docs/internal/feature-flags/fail-modes.md b/docs/internal/feature-flags/fail-modes.md new file mode 100644 index 000000000..b7a9ba2ec --- /dev/null +++ b/docs/internal/feature-flags/fail-modes.md @@ -0,0 +1,142 @@ +# Feature Flag Fail Modes + +How each feature flag evaluation method behaves when Unleash is unavailable or not configured, which code paths use each method, and the full evaluation precedence. + +## Evaluation Methods + +### Fail-Closed (default: `false`) + +These methods return `false` when Unleash is not configured or unreachable. Features gated behind these methods are **off by default** and require Unleash to be running and the flag to be explicitly enabled. + +| Method | Location | Signature | +|--------|----------|-----------| +| `IsEnabled` | `featureflags/featureflags.go:44` | `IsEnabled(flagName string) bool` | +| `IsEnabledWithContext` | `featureflags/featureflags.go:54` | `IsEnabledWithContext(flagName, userID, sessionID, remoteAddress string) bool` | +| `FeatureEnabled` | `handlers/featureflags.go:15` | `FeatureEnabled(flagName string) bool` | +| `FeatureEnabledForRequest` | `handlers/featureflags.go:22` | `FeatureEnabledForRequest(c *gin.Context, flagName string) bool` | + +`FeatureEnabled` and `FeatureEnabledForRequest` are convenience wrappers in the `handlers` package. `FeatureEnabled` calls `IsEnabled` directly. `FeatureEnabledForRequest` extracts user ID, session ID, and client IP from the Gin context and passes them to `IsEnabledWithContext`. They exist so handlers don't need to import the `featureflags` package. + +**Use for:** General features, runner type gates, experimental functionality, kill switches. If Unleash goes down, these features turn off. + +### Fail-Open (default: `true`) + +These methods return `true` when Unleash is not configured or unreachable. Features gated behind these methods are **on by default** and require Unleash to be running with the flag explicitly disabled to restrict them. + +| Method | Location | Signature | +|--------|----------|-----------| +| `IsModelEnabled` | `featureflags/featureflags.go:69` | `IsModelEnabled(flagName string) bool` | +| `IsModelEnabledWithContext` | `featureflags/featureflags.go:78` | `IsModelEnabledWithContext(flagName, userID, sessionID, remoteAddress string) bool` | + +**Use for:** Model availability flags. If Unleash goes down, all models remain available. The rationale is that blocking session creation due to flag infrastructure failure is worse than temporarily losing the ability to restrict a model. + +### Workspace-Aware Wrappers + +These methods check the workspace ConfigMap override first, then fall back to one of the above methods. They inherit the fail mode of their fallback. + +| Wrapper | Fallback | Fail Mode | Location | +|---------|----------|-----------|----------| +| `isModelEnabledWithOverrides` | `IsModelEnabled` | Fail-open | `handlers/models.go:149` | +| `isRunnerEnabledWithOverrides` | `FeatureEnabled` | Fail-closed | `handlers/runner_types.go:218` | + +## Where Each Method Is Used + +### `FeatureEnabled` / `IsEnabled` (fail-closed) + +| Caller | File | Purpose | +|--------|------|---------| +| `isRunnerEnabled` | `handlers/runner_types.go:208,213` | Check if a runner type is enabled via its feature gate | +| `GetRunnerTypesGlobal` | `handlers/runner_types.go:245` | Filter runners for admin listing (no workspace context) | + +### `FeatureEnabledForRequest` / `IsEnabledWithContext` (fail-closed) + +| Caller | File | Purpose | +|--------|------|---------| +| `EvaluateFeatureFlag` | `handlers/featureflags_admin.go:333` | Unleash fallback when no workspace ConfigMap override exists | + +### `IsModelEnabled` (fail-open) + +| Caller | File | Purpose | +|--------|------|---------| +| `isModelEnabledWithOverrides` | `handlers/models.go:155` | Fallback when no workspace ConfigMap override exists for a model flag | + +### `isModelEnabledWithOverrides` (fail-open via `IsModelEnabled`) + +| Caller | File | Purpose | +|--------|------|---------| +| `ListModelsForProject` | `handlers/models.go:123` | Filter feature-gated models in the model list endpoint | +| `isModelAvailable` | `handlers/models.go:237` | Validate model is enabled during session creation | + +### `isRunnerEnabledWithOverrides` (fail-closed via `FeatureEnabled`) + +| Caller | File | Purpose | +|--------|------|---------| +| `GetRunnerTypes` | `handlers/runner_types.go:280` | Filter runners by workspace-scoped feature flags | + +## Evaluation Precedence + +When a flag is evaluated for a workspace, the system checks three layers in order: + +``` +1. Workspace ConfigMap override (highest priority) + ConfigMap "feature-flag-overrides" in the workspace namespace + Key: flag name, Value: "true" or "false" + If present -> return that value, source: "workspace-override" + +2. Unleash SDK evaluation (middle priority) + Respects strategies, rollout percentages, A/B tests + User/session/IP context passed when available + If Unleash is configured -> return SDK result, source: "unleash" + +3. Code default (lowest priority, Unleash not configured) + General flags (IsEnabled): false (fail-closed) + Model flags (IsModelEnabled): true (fail-open) +``` + +### Precedence Table + +| ConfigMap Override | Unleash State | General Flag Result | Model Flag Result | +|--------------------|---------------|---------------------|-------------------| +| `"true"` | (any) | `true` | `true` | +| `"false"` | (any) | `false` | `false` | +| (not set) | enabled | `true` | `true` | +| (not set) | disabled | `false` | `false` | +| (not set) | 50% rollout | (evaluated per context) | (evaluated per context) | +| (not set) | (not configured) | `false` | `true` | +| (not set) | (unreachable) | `false` | `true` | + +## Frontend Fail Behavior + +The frontend has its own fail behavior independent of the backend: + +| Component | Behavior When Unleash Unavailable | +|-----------|----------------------------------| +| Next.js proxy (`/api/feature-flags`) | Returns `{ toggles: [] }` — all client-side flags `false` | +| `useFlag()` hook | Returns `false` (no toggles loaded) | +| `useVariant()` hook | Returns disabled variant | +| Feature Flags Admin UI | Shows "Feature Flags Not Available" message | +| Workspace flag evaluation (`/evaluate/:flagName`) | Falls through to backend fail mode (closed or open depending on method) | + +## Special Cases + +### Default Runner Fail-Open + +The default runner (`claude-code`) has a special fail-open path in `isRunnerEnabled` (`handlers/runner_types.go:204`). When the agent registry is unavailable, the default runner returns `true` to prevent blocking all session creation during cold start. + +### Default Model Bypass + +Default models (global `defaultModel` and per-provider `providerDefaults` from `models.json`) bypass feature flag checks entirely. They are always available regardless of flag state. This is enforced in both `ListModelsForProject` and `isModelAvailable`. + +### Model Manifest Unavailable + +When `models.json` cannot be read and no cached version exists: +- If a `requiredProvider` is specified (runner knows its provider): model is **rejected** to prevent cross-provider mismatches +- If no `requiredProvider` (registry also unavailable): model is **allowed** (fail-open for cold start) + +## Flag Naming Conventions + +| Category | Pattern | Example | Fail Mode | +|----------|---------|---------|-----------| +| Model flags | `model..enabled` | `model.claude-opus-4-6.enabled` | Fail-open | +| Runner flags | `runner..enabled` | `runner.gemini-cli.enabled` | Fail-closed | +| General flags | `..` | `frontend.file-explorer.enabled` | Fail-closed | diff --git a/docs/internal/feature-flags/feature-flags-unleash.md b/docs/internal/feature-flags/feature-flags-unleash.md index 3e631d433..3fe514f76 100644 --- a/docs/internal/feature-flags/feature-flags-unleash.md +++ b/docs/internal/feature-flags/feature-flags-unleash.md @@ -1,6 +1,6 @@ # Feature Flags with Unleash -The platform uses [Unleash](https://www.getunleash.io/) for optional feature toggles in the frontend and backend. When Unleash is not configured, all flags are disabled (safe default). +The platform uses [Unleash](https://www.getunleash.io/) for optional feature toggles in the frontend and backend. When Unleash is not configured, general flags are disabled (fail-closed) and model flags remain enabled (fail-open). See [fail-modes.md](fail-modes.md) for the full reference. ## Overview @@ -63,14 +63,16 @@ Set these for the **backend** (e.g. in deployment ConfigMap/Secret): | `UNLEASH_URL` | Yes* | Unleash server base URL (e.g. `https://unleash.example.com`) | | `UNLEASH_CLIENT_KEY` | Yes* | API token for the Unleash Client API (backend token; can be same or different from frontend) | -\*If either is missing, `featureflags.Init()` does nothing and all flag checks return `false`. +\*If either is missing, `featureflags.Init()` does nothing. General flag checks return `false` (fail-closed); model flag checks return `true` (fail-open). ### Usage in handlers -- **Global check** (same for all requests): `handlers.FeatureEnabled("flag-name")` -- **Per-request** (user/session/IP for strategies): `handlers.FeatureEnabledForRequest(c, "flag-name")` +- **Global check** (same for all requests): `handlers.FeatureEnabled("flag-name")` — fail-closed +- **Per-request** (user/session/IP for strategies): `handlers.FeatureEnabledForRequest(c, "flag-name")` — fail-closed +- **Model check**: `featureflags.IsModelEnabled("flag-name")` — fail-open (models stay available) +- **Model check with context**: `featureflags.IsModelEnabledWithContext("flag-name", userID, sessionID, remoteAddr)` — fail-open -When Unleash is not configured, both return `false`. +General flags return `false` when Unleash is not configured. Model flags return `true` so that model availability is not blocked by flag infrastructure outages. ### Example: enable/disable a feature (e.g. FakeFeature) @@ -112,9 +114,51 @@ Backend uses `github.com/Unleash/unleash-go-sdk/v5`. Ensure it is in `go.mod` an --- -## Admin UI +## Unleash Server UI -The platform includes a built-in admin UI for managing feature flags directly from the workspace. This allows users to view and toggle flags without accessing the Unleash dashboard. +The Unleash server has a built-in web UI for managing flags, strategies, API tokens, and projects. This is distinct from the workspace admin UI — the Unleash UI gives platform team members full control over all flags globally. + +### Accessing the UI + +**Local development (kind):** + +```bash +make unleash-port-forward +# Access at http://localhost:4242 +``` + +**OpenShift (local-dev / production):** + +The Unleash UI is exposed via an OpenShift Route: + +```bash +echo "https://$(oc get route unleash-route -n ambient-code -o jsonpath='{.spec.host}')" +``` + +### Default credentials + +On first startup, Unleash creates an `admin` user. The password is set by the `default-admin-password` key in the `unleash-credentials` secret. See `unleash-credentials-secret.yaml.example` for the template. + +### Common tasks + +- **Create API tokens:** Admin > API tokens. You need separate tokens for Admin API (`UNLEASH_ADMIN_TOKEN`), Client API (`UNLEASH_CLIENT_KEY`), and optionally Frontend API. +- **Create flags manually:** New feature toggle > type "release" > add strategies and tags. +- **Tag a flag as workspace-configurable:** Open the flag > Tags > add `scope:workspace`. The flag will then appear in the workspace admin UI. +- **View flag metrics:** Open a flag > Metrics tab to see evaluation counts and SDK usage. + +--- + +## Workspace Feature Flags (Ambient UI) + +The Ambient platform UI includes a built-in feature flags section within each workspace's settings. This is separate from the Unleash server UI — it only shows flags tagged `scope:workspace` and lets workspace admins set per-workspace overrides without affecting other workspaces or needing access to Unleash directly. + +### Evaluation Precedence + +Flag evaluation uses a three-tier system: + +1. **Workspace override (ConfigMap)** — highest priority. Stored in a `feature-flag-overrides` ConfigMap in the workspace namespace. +2. **Unleash global default** — fallback. Respects Unleash strategies, rollouts, and A/B tests. +3. **Code default** — absolute fallback. General flags: `false` (fail-closed). Model flags: `true` (fail-open). ### Environment Variables @@ -126,36 +170,55 @@ Set these for the **backend** to enable the Admin UI: | `UNLEASH_ADMIN_TOKEN` | Yes | Admin API token (from Unleash > Admin > API tokens) | | `UNLEASH_PROJECT` | No | Unleash project ID (default: `default`) | | `UNLEASH_ENVIRONMENT` | No | Target environment for toggles (default: `development`) | +| `UNLEASH_WORKSPACE_TAG_TYPE` | No | Tag type for workspace-configurable flags (default: `scope`) | +| `UNLEASH_WORKSPACE_TAG_VALUE` | No | Tag value for workspace-configurable flags (default: `workspace`) | **Note:** The Admin API token is different from the Client API token. Create one in Unleash UI > Admin > API tokens with Admin permissions. ### Using the Admin UI 1. Navigate to your workspace in the platform -2. Click **Feature Flags** in the sidebar -3. View all toggles with their current enabled/disabled state -4. Click the toggle switch to enable or disable a flag -5. Changes take effect immediately for new sessions +2. Click the **Workspace Settings** tab +3. Scroll down to the **Feature Flags** card at the bottom of the page +4. View all workspace-configurable flags grouped by category +4. Set overrides using the three-state control: **Default** (use platform value), **On** (force enable), **Off** (force disable) +5. Click **Save** to commit all pending changes (batch save pattern) +6. Click **Discard** to revert unsaved changes ### API Endpoints -The backend exposes these endpoints (proxied to Unleash Admin API): - | Endpoint | Method | Description | |----------|--------|-------------| -| `/api/projects/:projectName/feature-flags` | GET | List all feature toggles | -| `/api/projects/:projectName/feature-flags/:flagName` | GET | Get toggle details | -| `/api/projects/:projectName/feature-flags/:flagName/enable` | POST | Enable toggle in environment | -| `/api/projects/:projectName/feature-flags/:flagName/disable` | POST | Disable toggle in environment | +| `/api/projects/:name/feature-flags` | GET | List workspace-configurable flags with override status | +| `/api/projects/:name/feature-flags/evaluate/:flagName` | GET | Evaluate flag (ConfigMap override then Unleash fallback) | +| `/api/projects/:name/feature-flags/:flagName` | GET | Get single flag details from Unleash | +| `/api/projects/:name/feature-flags/:flagName/override` | PUT | Set workspace override (`{"enabled": bool}`) | +| `/api/projects/:name/feature-flags/:flagName/override` | DELETE | Remove workspace override (revert to Unleash default) | +| `/api/projects/:name/feature-flags/:flagName/enable` | POST | Enable flag (sets ConfigMap override to `"true"`) | +| `/api/projects/:name/feature-flags/:flagName/disable` | POST | Disable flag (sets ConfigMap override to `"false"`) | ### Example: Toggle a flag via API ```bash -# List all flags +# List all workspace-configurable flags curl -H "Authorization: Bearer $TOKEN" \ https://your-backend/api/projects/my-workspace/feature-flags -# Enable a flag +# Evaluate a flag for the workspace +curl -H "Authorization: Bearer $TOKEN" \ + https://your-backend/api/projects/my-workspace/feature-flags/evaluate/model.claude-opus-4-6.enabled + +# Set a workspace override +curl -X PUT -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"enabled": true}' \ + https://your-backend/api/projects/my-workspace/feature-flags/model.claude-opus-4-6.enabled/override + +# Remove a workspace override (revert to Unleash default) +curl -X DELETE -H "Authorization: Bearer $TOKEN" \ + https://your-backend/api/projects/my-workspace/feature-flags/model.claude-opus-4-6.enabled/override + +# Enable a flag (shorthand for setting override to true) curl -X POST -H "Authorization: Bearer $TOKEN" \ https://your-backend/api/projects/my-workspace/feature-flags/my-feature/enable @@ -166,6 +229,8 @@ curl -X POST -H "Authorization: Bearer $TOKEN" \ ### Reference -- `components/backend/handlers/featureflags_admin.go` – Admin API handlers +- `components/backend/featureflags/featureflags.go` – Unleash SDK init, fail-open/closed defaults +- `components/backend/handlers/featureflags_admin.go` – Admin API handlers, workspace override logic +- `components/backend/cmd/sync_flags.go` – Flag sync to Unleash at startup - `components/frontend/src/components/workspace-sections/feature-flags-section.tsx` – Admin UI component - `components/frontend/src/services/queries/use-feature-flags-admin.ts` – React Query hooks