Skip to content

fix(settings): keep member rows a true delta over org defaults - #243

Merged
jlav merged 1 commit into
jl/scope-member-settings-savefrom
jl/member-settings-true-delta
Aug 25, 2026
Merged

fix(settings): keep member rows a true delta over org defaults#243
jlav merged 1 commit into
jl/scope-member-settings-savefrom
jl/member-settings-true-delta

Conversation

@jlav

@jlav jlav commented Aug 25, 2026

Copy link
Copy Markdown
Member

HUMAN:

  • A human has tested these changes.

AGENT:


Why

Changing one setting detaches a member from every org-wide default, silently and permanently.

An admin switches the org onto a different model. It reaches the members who have never saved their agent settings. Everyone else stays on the old model with nothing telling them so, and the admin sees the change apply and assumes the rollout landed.

/settings/agent is the page that does it. Members can reach it (view_llm_settings is a member permission) and it has no read-only mode. Its save handler sends enable_sub_agents unconditionally, so hitting Save is enough on its own; there's no particular toggle a member has to find. The other two branches of that same handler send agent_kind, so that one page is the entry point for this and for #241.

The cause is that store() receives the member's composed settings: the org defaults with their own edits already merged in. Writing that whole view back turns every inherited value into an explicit override. I measured it on a member with an empty row who toggled enable_sub_agents and nothing else: their row went from 0 explicit settings to 10, including llm pinned to whatever the org default was at that moment.

Second thing in here: agent_context.current_datetime is a default_factory of datetime.now(), so it's regenerated on every construction. It was being written into the row on every save, and load() returns the stored value, so the agent gets handed a stale "now" on later conversations. Seeding a member row with 2019-01-01 and loading it returns 2019-01-01.

Approach

Before writing the row, drop anything that already matches the org default. What's left is a genuine override. What's dropped resolves through the org on load, so it keeps following that default when an admin changes it.

The comparison recurses, so changing one field stores that field alone rather than the whole block it lives in. A member who picks a model gets {'llm': {'model': ...}} and still tracks org changes to every other LLM field.

Both sides of the comparison go through one _agent_settings_dump helper. That matters more than it looks: api_key is excluded from the member's dump and mcp_config lives in its own column, so building the two sides separately made the blocks compare unequal and defeated the whole thing. Runtime values are stripped in the same place, which is what fixes the timestamp.

A consequence worth being explicit about: a member can't pin a value that currently equals the org default. Setting your model to the same model the org uses stores nothing, so you'll move when the org moves. Expressing "hold this even if the default changes" needs a separate flag, and nothing in the UI offers that today.

Issue Number

OHE-3138 - https://linear.app/all-hands-ai/issue/OHE-3138

How to Test

cd enterprise && poetry run pytest tests/unit/test_saas_settings_store.py

Manual, one admin and one member:

  1. As the member, change one setting that has nothing to do with the LLM, and save.
  2. As the admin, change the org's default model on /settings/org-defaults.
  3. As the member, reload settings. You should be on the new model, with your own change intact.

Video/Screenshots

Type

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

Notes

This is based on jl/scope-member-settings-save (#241) so the diff here is just the delta change. Retarget it to main if that one merges first.

It stacks on #241 rather than replacing it. The two fixes catch different things and neither covers the other:

Two tests from #241 changed here, which is the clearest way to see the difference:

  • test_non_flip_save_still_persists_full_agent_settings became test_non_flip_save_persists_only_the_changed_field. It pinned the full-dump write as deliberate; that's the behaviour this PR removes.
  • The kind-flip test no longer expects agent_kind in the row. Flipping back to openhands matches the org default, so it resolves by absence.

No migration. An existing row full of redundant overrides thins out on that member's next save.


Enterprise server image for this PR:

ghcr.io/openhands/enterprise-server:sha-c7a9e81

@linear

linear Bot commented Aug 25, 2026

Copy link
Copy Markdown
OHE-3138 Changing your own agent settings changes them for everyone in the org

What happens

If you change your own agent settings - switching your agent from OpenHands to Codex, say, or picking a different model - that change is applied to every other member of your organization, and to the organization's defaults.

There's no warning that this will happen, and you don't need to be an admin or owner to do it. Any member can.

For everyone else, their agent silently becomes whatever you picked. If your choice needs credentials they don't have, such as a connected Codex account or an API key, then every new conversation they try to start fails with an error like:

Connect your Codex account or set an API key before starting a Codex conversation.

They can't fix it for themselves either. If they change their own settings back, that just pushes their choice onto everyone else in turn.

What we saw

On our shared beta instance, one person switched their agent to Codex to try it out. That immediately blocked all 17 members of the organization from starting any new conversation, and it stayed broken until someone changed the setting back.

Switching the agent also wipes the organization's configured model and connection URL, and switching back doesn't restore them. So recovering isn't just a matter of undoing the change - someone has to know the original values and re-enter them by hand.

Expected behavior

  • Changing your own agent settings should only affect you.
  • Only an admin or owner should be able to change what the whole organization defaults to, and it should be obvious that's what they're doing.
  • Someone trying out a different agent shouldn't be able to stop their teammates from working.
  • Undoing a change should restore what was there before.

Impact

Any organization with more than one member. A single member with no special permissions can stop everyone else from starting conversations by changing a setting on their own account, and the people affected have no way to recover on their own.

Fix

The first three points are addressed in OpenHands/enterprise#241.

Review in Linear

@github-actions github-actions Bot added the type: fix A bug fix label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  enterprise/storage
  saas_settings_store.py 105, 129-138, 228-229, 239-250, 260-263, 756-762
Project Total  

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

@jlav
jlav merged commit c7a9e81 into jl/scope-member-settings-save Aug 25, 2026
15 of 16 checks passed
@jlav
jlav deleted the jl/member-settings-true-delta branch August 25, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant