Skip to content

feat(service-accounts): put service accounts behind the ENABLE_SERVICE_ACCOUNTS feature flag - #1088

Draft
henriquehirako wants to merge 5 commits into
feat/ohe-3713-service-accounts-uifrom
openhands/ohe-3795
Draft

henriquehirako wants to merge 5 commits into
feat/ohe-3713-service-accounts-uifrom
openhands/ohe-3795

Conversation

@henriquehirako

@henriquehirako henriquehirako commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

HUMAN:

  • A human has tested these changes.

AGENT:


Why

Service accounts must stay hidden until a Super Admin turns them on. ENABLE_SERVICE_ACCOUNTS is a database feature flag that falls back to an env var of the same name (default false), the same way ENABLE_BILLING works. The switch on the Super Admin Instance page writes the flag, so no redeploy is needed.

Summary

  • Backend gate. ENABLE_SERVICE_ACCOUNTS is now in _ENV_FLAG_DEFAULTS. The new validate_service_accounts_enabled() dependency on service_accounts_router returns 403 on every /api/organizations/{org_id}/service-accounts* route while the flag is off. It is a copy of validate_billing_enabled, and the 403 detail names the flag and the env var.
  • Web-client config. WebClientFeatureFlags.enable_service_accounts takes the env value as a fallback in _get_feature_flags. get_web_client_config then resolves it again with _resolve_flag('ENABLE_SERVICE_ACCOUNTS', ...), so a database row wins over the env var.
  • Frontend.
    • isSettingsPageHidden hides /settings/service-accounts (both the route loader and the nav entry) unless enable_service_accounts is true.
    • adminService gets createFeatureFlag and updateFeatureFlag.
    • New hook useSetServiceAccountsEnabled. It sends PATCH, and if that returns 404 it sends POST. On success it invalidates the web-client config query.
    • The Instance page gets a SettingsSwitch with testId="super-admin-instance-enable-service-accounts". The switch is disabled while a save is in progress.
    • New i18n key SUPER_ADMIN$INSTANCE_ENABLE_SERVICE_ACCOUNTS ("Enable Service Accounts (beta)").
Super Admin switch ──PATCH/POST /api/admin/feature-flags──▶ feature_flags row (ENABLE_SERVICE_ACCOUNTS)
                                                               │  (db row wins, else env var, else false)
                         ┌─────────────────────────────────────┴──────────────────────┐
     validate_service_accounts_enabled (403 when off)            GET /api/v1/web-client/config
     on every service_accounts_router route                       feature_flags.enable_service_accounts
                                                                   └▶ isSettingsPageHidden / settings nav

How each acceptance criterion is met

Criterion Where it is met Test
Flag off: every service_accounts_router route returns 403 Router-level Depends(validate_service_accounts_enabled) test_routes_return_403_when_flag_is_off checks all 10 routes. test_every_route_is_gated compares the tested routes with the router's by method and path, so a new route fails until it is added.
Flag off: /settings/service-accounts and its nav entry are hidden isSettingsPageHidden settings-utils.test.ts, use-settings-nav-items.test.tsx ("hides service accounts while enable_service_accounts is off"), service-accounts.test.tsx ("redirects an owner away while enable_service_accounts is off")
Flag on (env var or database row): no change from today. A database row wins over the env var. feature_flag_service.resolve test_routes_work_when_env_var_is_on[true/1], test_routes_work_when_database_row_is_on, test_database_row_wins_over_env_var. These run the real flag service against PostgreSQL. All existing route tests still pass with the gate open.
GET /api/v1/web-client/config returns feature_flags.enable_service_accounts, from the database row first, then the env var default_web_client_config_injector.py TestEnableServiceAccounts (real database): test_env_var_when_no_row, test_database_row_wins, test_toggle_takes_effect_without_restart
The Instance page switch has the label "Enable Service Accounts (beta)", shows the resolved value and writes the database flag SuperAdminInstance + useSetServiceAccountsEnabled super-admin-instance.test.tsx: label and value shown, PATCH sent on toggle, POST sent when PATCH returns 404, switch disabled while saving
Turning the flag on or off needs no redeploy Database flag. The admin endpoints invalidate the cache in their own process, and other processes see the change within the 5 s cache TTL. Local run below, plus test_toggle_takes_effect_without_restart

Issue Number

OHE-3795

How to Test

These are the commands I ran on this branch, with their results:

  • uv run pre-commit run --config ./dev_config/python/.pre-commit-config.yaml (on the changed files): ruff, ruff format and mypy all passed.
  • uv run pytest -n 4 --dist worksteal ./tests/unit: 9027 passed.
  • uv run pytest tests/unit/server/routes/test_service_accounts.py tests/unit/app_server/test_default_web_client_config_injector.py tests/unit/server/services/test_feature_flag_service.py tests/unit/test_billing.py: all passed.
  • cd frontend && npm run lint:fix && npm run lint (this includes tsc): passed, with 0 errors.
  • cd frontend && npm run build: passed.
  • cd frontend && npx vitest run: 3260 passed across 360 test files (2 skipped), with 0 failures.
    • Vitest also reported a few "Unhandled Rejection: ProgressEvent is not defined" errors from MSW's XHR interceptor during test teardown.
    • These already happen on the base branch. A full run of feat/ohe-3713-service-accounts-ui gave 3 such errors (from app-settings, agent-settings and interactive-chat-box).
    • On this branch, one run attributed a 4th error to service-accounts.test.tsx. That file passed with no error when run on its own, twice.
  • Red first: I removed the implementation and the new tests failed: 11 backend gate tests and 9 frontend tests. With the implementation back, they pass.

Local run (switch on → off → on, one server process, no restart)

Setup:

  • PostgreSQL from make local-db, migrated to head.
  • The real feature_flag_router, service_accounts_router, FeatureFlagService and DefaultWebClientConfigInjector.
  • Keycloak was not available in the sandbox, so I stubbed only the identity and role lookups, to act as a Super Admin who owns the org.
  • The calls are the same ones the Instance switch sends:
--- initial: no env var, no database row
GET /api/v1/web-client/config        -> {"feature_flags":{"enable_service_accounts":false}}
GET /api/organizations/{org}/service-accounts -> 403 {"detail":"Service accounts are disabled in this environment. Enable the ENABLE_SERVICE_ACCOUNTS feature flag (or the ENABLE_SERVICE_ACCOUNTS environment variable) to use them."}
PATCH /api/admin/feature-flags/ENABLE_SERVICE_ACCOUNTS {"enabled":true} -> {"detail":"Flag not found"} [404]
POST /api/admin/feature-flags {"key":"ENABLE_SERVICE_ACCOUNTS","description":"Enable Service Accounts (beta)","enabled":true} -> {...,"enabled":true,"rules":[]} [201]
--- after switch ON (no restart)
GET /api/v1/web-client/config        -> {"feature_flags":{"enable_service_accounts":true}}
GET /api/organizations/{org}/service-accounts -> 200 {"items":[],"next_page_id":null}
PATCH /api/admin/feature-flags/ENABLE_SERVICE_ACCOUNTS {"enabled":false} -> {...,"enabled":false} [200]
--- after switch OFF (no restart)
GET /api/v1/web-client/config        -> {"feature_flags":{"enable_service_accounts":false}}
GET /api/organizations/{org}/service-accounts -> 403 {...}
PATCH /api/admin/feature-flags/ENABLE_SERVICE_ACCOUNTS {"enabled":true} -> {...,"enabled":true} [200]
--- after switch ON again (no restart)
GET /api/v1/web-client/config        -> {"feature_flags":{"enable_service_accounts":true}}
GET /api/organizations/{org}/service-accounts -> 200 {"items":[],"next_page_id":null}

In the frontend, the nav entry and the route follow feature_flags.enable_service_accounts through isSettingsPageHidden. The mutation invalidates the config query, so the page picks up the change without a reload.

I did not test the full UI end to end in a browser: the SaaS stack (Keycloak and the Super Admin login) was not available in this sandbox.

Video/Screenshots

None. See the local run output above.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • Default is off. Installs that use service accounts today must turn the switch on, or set ENABLE_SERVICE_ACCOUNTS=true.
  • Env value parsing. The env fallback in _get_feature_flags uses _env_flag_enabled (accepts true or 1). The ticket text used == 'true', but the repo's AGENTS.md rule requires accepting both values. This also matches what the backend gate accepts.
  • Not gated: API key hardening, the identity model, migrations, guards and the automation service, as the ticket says.
  • Known limits (from the ticket):
    • Turning the flag off does not stop automations that service accounts already own.
    • A direct automation API call can still transfer an automation to an existing service account.
    • The flag cache TTL is 5 s per process.
    • While the flag is off, the service account routes (including Disable) return 403. To stop a service account then, a Super Admin disables it from the user directory, or turns the flag on first.
    • While the flag is off, org budgets still list service account rows, and admins can still set their budget. This keeps budget control over automations that keep running.
  • Test helper changes. The shared createMockWebClientConfig default is unchanged (enable_service_accounts is absent, so the flag reads as off). The existing service-account route tests and nav tests now turn the flag on explicitly.

This PR was created by an AI agent (OpenHands) on behalf of the ticket author.

@henriquehirako can click here to continue refining the PR


Enterprise server image for this PR:

ghcr.io/openhands/enterprise-server:sha-1b74ed8

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  openhands/app_server/web_client
  default_web_client_config_injector.py
  web_client_models.py
  server/routes
  service_accounts.py
  server/services
  feature_flag_service.py
Project Total  

This report was generated by python-coverage-comment-action

@henriquehirako
henriquehirako force-pushed the feat/ohe-3713-service-accounts-ui branch 2 times, most recently from 7c26041 to 6792edc Compare October 30, 2026 14:06
Gate every service_accounts_router route on the ENABLE_SERVICE_ACCOUNTS
database flag (env var fallback, default off), expose it in the web-client
config, hide /settings/service-accounts while it is off, and add a Super
Admin Instance switch that writes the database flag.

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
@henriquehirako
henriquehirako force-pushed the feat/ohe-3713-service-accounts-ui branch from 6792edc to 6280145 Compare October 30, 2026 20:56
…nfig refetch returns

The switch was enabled again with the old value while the config refetched.
It now shows the saved value at once and stays disabled until the refetch
finishes.
@henriquehirako henriquehirako changed the title [OHE-3795] Put service accounts behind the ENABLE_SERVICE_ACCOUNTS feature flag (enterprise) feat(service-accounts): put service accounts behind the ENABLE_SERVICE_ACCOUNTS feature flag Oct 30, 2026
@github-actions github-actions Bot added the type: feat A new feature label Oct 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants