Add visibility-based defaults for model access levels
What does this MR do and why?
New projects now get visibility-based defaults for the model registry and
model experiments access levels, mirroring how pages_access_level works:
- Public projects →
Enabled(Everyone With Access) - Internal / Private projects →
Private(Only Project Members)
Previously both features always defaulted to Enabled regardless of project
visibility.
Implementation:
- Drop the DB-level
DEFAULT 20onproject_features.model_registry_access_levelandmodel_experiments_access_level(metadata-only migration; existing rows untouched). - Compute the default in an
after_initializecallback onProjectFeature, so a new record startsNULLand the callback fills in the value from project visibility (explicitly-provided values are preserved). - Update the development seed fixture to set these columns explicitly, since it relied on the dropped default.
Steps to reproduce
- Run the migrations.
- Create a new public project. Open Settings > General, expand Visibility, project features, permissions. The Model registry and Model experiments access dropdowns show Everyone With Access.
- Create a new private project (and repeat for internal). In the same settings, Model registry and Model experiments show Only Project Members.
Before this change, all three visibilities showed Everyone With Access.
Does this affect existing behavior?
No. ProjectPolicy has a carve-out that, on non-public projects, only grants
read_model_registry / read_model_experiments to members (guest and above),
regardless of the feature's access level:
rule { ~public_project & guest & model_registry_enabled }.enable :read_model_registrySo non-members were already prevented from reading these features on
internal/private projects even while the level defaulted to Enabled. Lowering
the default to Private doesn't change who can read — it just aligns the stored
default (and the settings dropdown above) with what the policy already enforces.
References
Migration
Post-deployment migration. The default is changed in a post-deploy migration
(and the model declares columns_changing_default via SafelyChangeColumnDefault)
per Changing column defaults.
change_column_default is a metadata-only operation in PostgreSQL — no table
rewrite, existing rows untouched. Runs identically on the main, ci, and
sec databases; main output shown below.
Up
$ bundle exec rails db:migrate:up:main VERSION=20260617130000
main: == 20260617130000 RemoveModelFeaturesAccessLevelDefaults: migrating ===========
main: -- change_column_default(:project_features, :model_registry_access_level, {:from=>20, :to=>nil})
main: -- change_column_default(:project_features, :model_experiments_access_level, {:from=>20, :to=>nil})
main: == 20260617130000 RemoveModelFeaturesAccessLevelDefaults: migrated (0.0182s) ==Down
$ bundle exec rails db:migrate:down:main VERSION=20260617130000
main: == 20260617130000 RemoveModelFeaturesAccessLevelDefaults: reverting ===========
main: -- change_column_default(:project_features, :model_experiments_access_level, {:from=>nil, :to=>20})
main: -- change_column_default(:project_features, :model_registry_access_level, {:from=>nil, :to=>20})
main: == 20260617130000 RemoveModelFeaturesAccessLevelDefaults: reverted (0.0201s) ==Unrelated change: trailing newlines on ai_agent schema migration markers
This MR also strips a trailing newline from four db/schema_migrations/ marker
files unrelated to this change:
20260616100000,20260616100100,20260616100200,20260616100300(the ai_agent table-drop migrations)
Why: those four markers were committed to master with a trailing newline,
but db:schema:dump generates schema-migration markers without one. The
db:check-migrations job regenerates every marker and fails on any difference,
so it was failing on this MR (and would fail on any MR that adds a migration)
on those four files — nothing to do with the project_features change here.
Stripping the trailing newline makes the committed files match the generated
format and unblocks the pipeline. Checksums are unchanged (newline-only diff).