Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/30204-feature-flags-namespace-removed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@objectstack/service-settings': patch
'@objectstack/platform-objects': patch
---

fix(service-settings)!: Setup no longer offers a Feature Flags page whose four toggles changed nothing (#30204)

Clause-②: no (narrowing) — removes the four settable keys of the `feature_flags` settings namespace (`ai_enabled`, `kanban_swimlanes`, `realtime_cursors`, `inline_comments`), the namespace itself (left empty by their removal), its Setup entry and the `featureFlagsSettingsManifest` export. Nothing is widened.
Metadata-protocol: none

<!-- adr-0087: not-required (runtime-export-retired @objectstack/service-settings#featureFlagsSettingsManifest) The one author-reachable removal is a runtime export: the `feature_flags` settings manifest constant leaves `@objectstack/service-settings`'s published entry and `builtinSettingsManifests`. A settings key is a value an administrator saves, not metadata an author writes: no spec key, authorable spelling, object definition or stored shape is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->

**BREAKING** for an administrator who saved one of the four Feature Flags toggles, and for a host that imports `featureFlagsSettingsManifest`.

- **Why.** No code read any of the four keys, so turning one on changed nothing (ADR-0049 enforce-or-remove). A flag comes back with the feature that reads it.
- **Removed keys.** `feature_flags.ai_enabled`, `feature_flags.kanban_swimlanes`, `feature_flags.realtime_cursors` and `feature_flags.inline_comments`: removed; each had no effect. The in-app AI panel is decided elsewhere and is unchanged.
- **The namespace.** `feature_flags` is no longer registered, so `GET` and `PUT /api/settings/feature_flags` answer `404 UNKNOWN_NAMESPACE`, and `SettingsService.getManifest('feature_flags')` throws `UnknownNamespaceError` (`SETTINGS_UNKNOWN_NAMESPACE`). Setup's Configuration group no longer lists Feature Flags (`@objectstack/platform-objects`, entry `nav_settings_feature_flags` and its four locale labels).
- **Stored values.** A `sys_setting` row saved under `feature_flags` and an `OS_FEATURE_FLAGS_*` environment variable are left in place and become inert: nothing registers the namespace, so nothing resolves them. They are not cleaned up; delete them at will.

The one-line fix for a host:

- FROM `featureFlagsSettingsManifest` TO nothing: drop it from the host's `SettingsServicePlugin({ manifests })` list; `builtinSettingsManifests` no longer carries it.
4 changes: 2 additions & 2 deletions content/docs/protocol/kernel/config-resolution.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,8 @@ derived mechanically from `(namespace, key)` by `envKeyOf`: uppercase, `.` and
# namespace 'crm', key 'api_key'
OS_CRM_API_KEY=sk_live_...

# namespace 'feature_flags', key 'ai-enabled'
OS_FEATURE_FLAGS_AI_ENABLED=true
# namespace 'ai', key 'log-prompts'
OS_AI_LOG_PROMPTS=true
```

The raw string is coerced by the **type of the specifier's default**
Expand Down
2 changes: 1 addition & 1 deletion content/docs/ui/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ The Setup app's anchors:
| `group_people_org` | Users, organizations, business units, teams, invitations |
| `group_access_control` | Positions, permission sets, sharing rules, API keys |
| `group_approvals` | Approvals inbox, requests, history, delegations |
| `group_configuration` | Settings, localization, exchange rates, authentication, email, storage, AI, feature flags, branding |
| `group_configuration` | Settings, localization, exchange rates, authentication, email, storage, AI, branding |
| `group_diagnostics` | Sessions, notification events, audit log |
| `group_integrations` | Webhooks, cloud connection |
| `group_advanced` | OAuth applications |
Expand Down
11 changes: 5 additions & 6 deletions docs/qa/platform-checklist/areas/platform-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@
"as admin, GET /api/v1/meta/app?id=setup and extract every merged nav destination — Setup is a SHELL of empty group anchors (packages/platform-objects/src/apps/setup.app.ts: group_overview/apps/people_org/access_control/approvals/configuration/diagnostics/integrations/advanced) filled by SETUP_NAV_CONTRIBUTIONS (setup-nav.contributions.ts) plus capability-plugin contributions (plugin-security Positions/Permission Sets, plugin-sharing Sharing Rules/Record Shares, plugin-approvals, plugin-webhooks)",
"GET /api/v1/meta/app with NO query first and record the names actually served — a stock boot answers exactly showcase_app, setup, account. Then GET /api/v1/meta/app?id=account (account.app.ts — Profile + Inbox/Security/Developer groups). ⚠️ GET /api/v1/meta/app?id=studio answers 200 {\"items\":[]} on every stock boot and that is the expected result (knownGap): record the absence as an assertion and move on — do NOT install @objectstack/studio to make the step runnable",
"as admin, hand-walk each destination in each app. At a window narrower than 1416 px (1280 px included) the console folds the sidebar to its icon rail, and Setup and Account open that way (ui/packages/app-shell/src/layout/consoleSidebarRail.ts CONSOLE_SIDEBAR_RAIL_BELOW_WIDTH, objectstack#27593; QA run #29877 met it at 1280 px): press the sidebar toggle at the app bar's left edge (Ctrl/⌘+B) to unfold it before expanding any group. EXPAND every nav group first: some render collapsed by default — Setup's Advanced group and Account's Developer group declare `expanded: false` (packages/platform-objects/src/apps/setup.app.ts, account.app.ts), and QA run #24557 found Setup's Configuration group collapsed as well — so a walk that never expands them never reaches their entries. Then navigate, wait for render, SCREENSHOT FIRST, then read the DOM — assert no pageerror, no 'failed to load', no blank <main>, no 'no actions configured' placeholder",
"Setup detail: confirm each settings URL entry (nav_settings_* → /apps/setup/system/settings/<namespace>) opens the settings namespace form — the 11 namespaces of a stock single-posture boot, localization/company/branding/auth/mail/sms/storage/ai/knowledge/lifecycle/feature_flags: the nav_settings_* entries of packages/platform-objects/src/apps/setup-nav.contributions.ts, plus nav_settings_branding, which packages/services/service-settings/src/settings-service-plugin.ts contributes only where no organization wall stands, and Users / Organization / Business Units / Teams / Sessions / OAuth Applications render. User preferences and identity links are not Setup destinations: open a user from Users and confirm the user page's Preferences and Linked Accounts tabs render that user's rows",
"Setup detail: confirm each settings URL entry (nav_settings_* → /apps/setup/system/settings/<namespace>) opens the settings namespace form — the 10 namespaces of a stock single-posture boot, localization/company/branding/auth/mail/sms/storage/ai/knowledge/lifecycle: the nav_settings_* entries of packages/platform-objects/src/apps/setup-nav.contributions.ts, plus nav_settings_branding, which packages/services/service-settings/src/settings-service-plugin.ts contributes only where no organization wall stands, and Users / Organization / Business Units / Teams / Sessions / OAuth Applications render. User preferences and identity links are not Setup destinations: open a user from Users and confirm the user page's Preferences and Linked Accounts tabs render that user's rows",
"Studio detail is NOT walked from here — the app is not registered on a stock boot (knownGap), so there is no merged nav to render. Its metadata:resource lists and component surfaces (App Builder, All Metadata Types, Packages, API Console, Flow Runs, Public Forms) are reached through the console surface at /_console/studio/<pkg>/<pillar> and are covered by area studio-authoring. What THIS item asserts about Studio is the absence itself, per clause 6",
"Account detail: confirm Profile (account:profile_card), Notifications (sys_inbox_message/mine), Approvals (nav_account_approvals, the approvals:inbox component, served as …/component/approvals/inbox — the Approvals Inbox, not the sys_approval_request grid; account.app.ts), Linked Accounts (sys_account), Active Sessions (sys_session/mine), API Keys (sys_api_key/mine), OAuth Applications (sys_oauth_application/mine) and Connect an Agent (nav_connect_agent) each render — 8 Account destinations and 46 Setup destinations for the admin on a stock single-org showcase boot, as QA run #29877 read them at 589049b0 and a fresh showcase boot re-read them at 232d3bb5ef (GET /api/v1/meta/app leaves, which match the nav source minus its gates). Since QA run #28545's reading Setup's Packaged automation page retired under ADR-0146 D11, leaving Packaged Actions (nav_packaged_actions, automation:packaged-actions; #28785), and its unfiltered Packages entry moved to Studio (#29547) (47 for #28927, 50 at eeec88d1 for QA run #28545, 48 re-measured at 3fd80337 for #28338, QA run #27481 read 44 and 8, QA run #24557 read 42 at 5b14b7f3, 45 on 2026-10-01, and #9453 recorded 43 and 7). Recount from the payload each run",
"confirm the gated entries resolve to ABSENT-not-erroring for the admin: nav_organizations (requiresService org-scoping) is absent in single-org mode; nav_jwks Signing Keys is ABSENT for EVERY persona, admin included — the entry was removed in #7544 because sys_jwks declares apiEnabled:false, so its list answers OBJECT_API_DISABLED (404) for all callers and no permission gate could prune it; SSO Providers is absent unless OS_SSO_ENABLED (knownGap)",
Expand Down Expand Up @@ -785,15 +785,15 @@
"fixtures": {
"app": "showcase",
"requires": [
"the 11 open-framework settings namespaces a stock boot serves: the 10 in builtinSettingsManifests (packages/services/service-settings/src/manifests: localization, company, branding, auth, mail, sms, storage, ai, knowledge, feature_flags) PLUS lifecycle (5 specifiers), which is NOT in that aggregate — ObjectQLPlugin registers it at kernel:ready whenever a SettingsService is present (packages/objectql/src/lifecycle/lifecycle-settings.ts). Enumerating only the manifests directory undercounts by one",
"the 10 open-framework settings namespaces a stock boot serves: the 9 in builtinSettingsManifests (packages/services/service-settings/src/manifests: localization, company, branding, auth, mail, sms, storage, ai, knowledge) PLUS lifecycle (5 specifiers), which is NOT in that aggregate — ObjectQLPlugin registers it at kernel:ready whenever a SettingsService is present (packages/objectql/src/lifecycle/lifecycle-settings.ts). Enumerating only the manifests directory undercounts by one",
"the settings service wired with a data engine so it swaps from in-memory to the sys_setting table, and with the sys_secret store + auditWriter (SettingsServicePlugin binds these on kernel:ready; the showcase stack mounts the engine)"
],
"knownGaps": [
"HONORING the ai.json caveat: a saved settings FORM is NOT proof of effect — the AI namespace's test action is a stub validator until @objectstack/service-ai is mounted, and a saved ai.provider does nothing observable without it. The effect clause therefore uses localization, whose consumer (resolve-authz-context.ts reads localization.timezone/locale/currency onto every ExecutionContext) is stock and observable; branding.workspace_name in the shell is the browser fallback. Never tick effect off a persisted form."
]
},
"steps": [
"as admin, GET /api/settings — capture the visible manifests (the 11 open-framework namespaces above; the route is the SettingsService REST surface in settings-routes.ts)",
"as admin, GET /api/settings — capture the visible manifests (the 10 open-framework namespaces above; the route is the SettingsService REST surface in settings-routes.ts)",
"pick localization (an OBSERVABLE-CONSUMER namespace — packages/core/src/security/resolve-authz-context.ts reads localization.timezone/locale/currency onto ExecutionContext). GET /api/settings/localization — capture {manifest, values}; note the timezone value and its source. On an `os dev --seed-admin` boot the dev seed has ALREADY written the host's zone (UTC in these containers) at tenant scope (packages/plugins/plugin-auth/src/auth-plugin.ts, #23175), so source reads 'tenant' before any PUT; only a boot without the dev seed starts at 'default'",
"change it: PUT /api/settings/localization { timezone: 'Asia/Tokyo' }; re-read GET /api/settings/localization and confirm the value persists and source is the scope, 'tenant' (localization scope is 'tenant'): on a dev-seeded boot the source already read 'tenant', so the value is what round-trips; on an unseeded boot the source flips from 'default'. For the console badge (steps[5]) pick a localization key the dev seed did NOT write (e.g. default_country), so its source starts at 'default'",
"prove EFFECT (not just the saved form): re-derive an ExecutionContext-dependent output and confirm it now resolves against the saved zone, not UTC. Two stock consumers serve: GET /api/v1/auth/me/localization (the resolved timezone the session carries), and a datetime the console renders — e.g. showcase_field_zoo's f_datetime on its record page, which shifts by exactly the zone offset. ⛔ The analytics date bucket has no stock observable (the only date-typed time dimension is due_date, and the seeded admin holds no export grant), so do not reach for it. If no localization effect is observable at all, fall back to branding.workspace_name visible in the console shell — but never tick effect off the form alone",
Expand Down Expand Up @@ -882,7 +882,7 @@
"evidence": "the two 401 bodies"
},
{
"clause": "each of the 11 open-framework namespaces resolves GET /api/settings/:ns with its manifest + values — no namespace 500s or serves an empty manifest",
"clause": "each of the 10 open-framework namespaces resolves GET /api/settings/:ns with its manifest + values — no namespace 500s or serves an empty manifest",
"oracle": "api",
"verify": "one GET per variant in variants[]; each returns {manifest, values} with the manifest's specifiers",
"evidence": "the per-namespace responses"
Expand All @@ -907,8 +907,7 @@
"sms",
"storage",
"ai",
"knowledge",
"feature_flags"
"knowledge"
],
"traps": ["stale-console-bundle", "dispatcher-vs-hono-route"],
"source": [
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ service plugins for every preset **except** `--preset minimal`:
| `queue` | `QueueServicePlugin` | Background retries (mail, audit batching) need a queue. |
| `job` | `JobServicePlugin` | Scheduled jobs (cleanup, reports, cron). |
| `cache` | `CacheServicePlugin` | In-memory adapter; performance baseline. |
| `settings` | `SettingsServicePlugin` | Mail / storage / branding / feature-flag UI + persistence. |
| `settings` | `SettingsServicePlugin` | Mail / storage / branding settings UI + persistence. |
| `email` | `EmailServicePlugin` | Auth callbacks (verify, reset, magic-link) hard-depend. |
| `storage` | `StorageServicePlugin` | Avatars, attachments, exports. Local fallback in dev. |

Expand All @@ -437,8 +437,8 @@ Apps no longer need to list these in `requires: [...]`.
**Opt-out:** run `objectstack serve --preset minimal` to skip the slate
and only load core + your explicit `requires`.

**Built-in Settings manifests:** `mail`, `storage`, `branding`,
`feature_flags` are pre-registered. Both `mail` and `storage` expose a
**Built-in Settings manifests:** `mail`, `storage` and `branding`
are among those pre-registered. Both `mail` and `storage` expose a
"Test connection" action that exercises the live transport / adapter
end-to-end. Switching the storage adapter does **not** migrate
previously uploaded files — the plugin logs a warning on every swap.
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/test/connect-agent-both-halves-wire.pin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,15 +254,16 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', (
.toContain('nav_connect_agent');
// The Setup tree the platform admin is served BY THIS COMPOSITION: 9
// authored groups less `group_approvals`, which nothing contributes
// into and the filter collapses, plus the 24 entries
// into and the filter collapses, plus the 23 entries
// `SETUP_NAV_CONTRIBUTIONS` carries, plus the MCP one. (25 until
// #25098 moved the unfiltered Packages entry to Studio.)
// #25098 moved the unfiltered Packages entry to Studio; 24 until
// #30204 removed the Feature Flags entry with its namespace.)
// `nav_settings_branding` is not among them, and not because of a
// tenancy wall (this harness boots none): since #24766 it is
// contributed by `@objectstack/service-settings` together with the
// `branding` namespace, offered only where no organization wall stands,
// and this harness composes platform-objects and the MCP bundle alone.
expect(allowedIds).toHaveLength(33);
expect(allowedIds).toHaveLength(32);
expect(allowedIds).not.toContain('nav_settings_branding');
// The two per-user stores are reached from the User record page, not
// from Setup's menu (#23803).
Expand All @@ -285,8 +286,8 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', (
expect(withAccountHalf.statusCode).toBe(200);
expect(withoutAccountHalf.statusCode).toBe(200);
expect(navIds(withAccountHalf.body?.item)).toEqual(navIds(withoutAccountHalf.body?.item));
// 33: the composition the count in the test above states.
expect(navIds(withAccountHalf.body?.item)).toHaveLength(33);
// 32: the composition the count in the test above states.
expect(navIds(withAccountHalf.body?.item)).toHaveLength(32);

// The mechanism behind the invariance, asserted rather than assumed:
// the bundle aims exactly one contribution at `setup` and one at
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/src/connect-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* entry stays for admins. ⛔ Ungating Setup is NOT the fix and was measured:
* the app-level `setup.access` gate fires BEFORE the group gate, so dropping
* the group gate alone changes nothing, and dropping both serves 14+ unrelated
* Setup surfaces (Users, Organization, Branding, Feature Flags, …) to every
* Setup surfaces (Users, Organization, Branding, Company, …) to every
* signed-in user — far past what this card asks for. ⛔ Nor is a
* `requiresService: 'mcp'` gate on an `account.app.ts` entry: the `mcp` service
* registers unconditionally in `init()` while this bundle registers behind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [
// `lifecycle` is `@objectstack/objectql`'s manifest (ADR-0057), offered
// wherever a settings service is mounted, like the entries above.
{ id: 'nav_settings_lifecycle', type: 'url', label: 'Data Lifecycle', url: '/apps/setup/system/settings/lifecycle', icon: 'timer', requiredPermissions: ['manage_platform_settings'] },
{ id: 'nav_settings_feature_flags', type: 'url', label: 'Feature Flags', url: '/apps/setup/system/settings/feature_flags', icon: 'flag' },
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3388,7 +3388,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
namespace: {
label: "Namespace",
help: "Manifest namespace (e.g. mail, branding, feature_flags)."
help: "Manifest namespace (e.g. mail, branding, storage)."
},
key: {
label: "Key",
Expand Down
1 change: 0 additions & 1 deletion packages/platform-objects/src/apps/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ export const en: TranslationData = {
nav_settings_ai: { label: 'AI & Embedder' },
nav_settings_knowledge: { label: 'Knowledge' },
nav_settings_lifecycle: { label: 'Data Lifecycle' },
nav_settings_feature_flags: { label: 'Feature Flags' },
// Notifications (contributed by @objectstack/service-messaging)
nav_notification_preferences: { label: 'Notification Preferences' },
nav_notification_templates: { label: 'Notification Templates' },
Expand Down
Loading
Loading