Skip to content

feat(BA-6555): AppConfig merge engine + resolve service (service layer)#12359

Draft
jopemachine wants to merge 1 commit into
feat/app-config-fragment-rbac-write-basefrom
feat/BA-6555-app-config-merge-engine
Draft

feat(BA-6555): AppConfig merge engine + resolve service (service layer)#12359
jopemachine wants to merge 1 commit into
feat/app-config-fragment-rbac-write-basefrom
feat/BA-6555-app-config-merge-engine

Conversation

@jopemachine

@jopemachine jopemachine commented Jun 23, 2026

Copy link
Copy Markdown
Member

📚 Stacked PRs

Part of the AppConfigFragment / AppConfig stack under BEP-1052 (epic BA-5781). Merge in order:

  1. feat(BA-6552): add app_config_fragments DB model and Alembic migration #12306feat(BA-6552): app_config_fragments DB model and Alembic migration
  2. feat(BA-6553): add app_config_fragments repository layer #12307feat(BA-6553): repository layer
  3. refactor(BA-6619): consolidate AppConfigScopeType into common.data (single definition) #12403refactor(BA-6619): consolidate AppConfigScopeType into common.data
  4. refactor(BA-6620): ExistsQuerier ops primitive + AppConfigAllowList.exists #12405refactor(BA-6620): ExistsQuerier + AppConfigAllowList.exists
  5. feat(BA-6554): add app_config_fragment service layer #12358feat(BA-6554): AppConfigFragment service layer
  6. feat(BA-6702): move fragment rank to the allow list with scope defaults #12516feat(BA-6702): move fragment rank to the allow list with scope defaults
  7. feat(BA-6701): expose allow-list rank on the v2 API surface #12517feat(BA-6701): expose allow-list rank on the v2 API surface
  8. feat(BA-6704): cascade app config subtree deletion from the definition #12518feat(BA-6704): cascade app config subtree deletion from the definition
  9. feat(BA-6626): app_config_fragment bulk repository layer #12426feat(BA-6626): app_config_fragment bulk repository layer
  10. feat(BA-6618): app_config_fragment bulk CRUD service layer #12401feat(BA-6618): app_config_fragment bulk CRUD service layer
  11. feat(BA-6810): AppConfigFragment visible-fragments query (repository layer) #12706feat(BA-6810): AppConfigFragment visible-fragments query (repository layer)
  12. feat(BA-6886): register APP_CONFIG_FRAGMENT as an RBAC resource type #12856feat(BA-6886): register APP_CONFIG_FRAGMENT as an RBAC resource type + permission backfill
  13. feat(BA-6859): bind AppConfig fragments to their RBAC scope on write (repository layer) #12826feat(BA-6859): bind fragments to their RBAC scope on write (repository layer)
  14. fix(BA-6872): seed APP_CONFIG_FRAGMENT permissions into the RBAC role fixture #12837fix(BA-6872): seed APP_CONFIG_FRAGMENT permissions into the RBAC role fixture
  15. chore(BA-6873): drop the dead APP_CONFIG RBAC entity type #12839chore(BA-6873): drop the dead APP_CONFIG RBAC entity type
  16. 👉 feat(BA-6555): AppConfig merge engine + resolve service (service layer) #12359feat(BA-6555): AppConfig merge engine + resolve service (service layer) ← you are here
  17. feat(BA-6556): AppConfig REST v2 API (raw fragments and merged read/update) #12377feat(BA-6556): AppConfig REST v2 API
  18. feat(BA-6860): RBAC-gate app_config fragment writes at the processors #12759feat(BA-6860): fragment write API routes + bulk write authz

Access model: fragment create/update/purge are gated by RBAC (a user writes their own user-scope fragment, a domain admin their domain's, a superadmin any — public is superadmin-only); reads are gated by the allow list (existence = readable layer, rank = merge order) plus scope visibility, with no RBAC on the read path; app_config_definitions / app_config_allow_list themselves are superadmin-only (REST + GQL). Closed / superseded: #12794 · #12756 · #12764.

Summary

Adds the service / merge layer of the merged AppConfig read (BA-6555), on top of the repository query in #12706. Resolves the merged AppConfig for a (user, config_name) principal by deep-merging, in rank order, the fragments that #12706's list_visible_fragments returns. This is the read foundation the REST/GQL endpoints (BA-6556, #12377) build on.

Split from the original combined PR: the repository read (visibility conditions, AppConfigScopeArguments, list_visible_fragments[_bulk], real-DB query tests) now lives in #12706; this PR is the service/merge layer only.

What's included

  • Merged view type (data/app_config/types.py): AppConfigData — the ordered contributing fragments (rank low → high) plus their deep-merged merged_config. merged_config is None when no fragment contributes (a defined-but-unconfigured name), distinct from a fragment that merges to an empty {}.
  • Merge engine (AppConfigService._merge_configs / _deep_merge_replace): fragments deep-merged in ascending rank — nested dicts recurse, but lists and scalars are replaced wholesale by the higher-rank fragment (a user's list fully overrides the lower scope's, not element-by-index). Intentionally not common.utils.deep_merge, which index-merges lists.
  • Service actions: resolve (one config_name) and resolve_bulk (several names for one principal, one query per distinct scope). Both are USER-scope GET actions returning AppConfigData in request order; a name repeated in resolve_bulk is repeated in the output.
  • Tests: deep-merge semantics (recurse / wholesale list replace / None-projection / rank order), plus the resolve / resolve_bulk service paths against a mocked repository.

The HTTP surface (raw fragments + merged read/update) lands in BA-6556 (REST v2, #12377).


📚 Documentation preview 📚: https://sorna--12359.org.readthedocs.build/en/12359/


📚 Documentation preview 📚: https://sorna-ko--12359.org.readthedocs.build/ko/12359/


📚 Documentation preview 📚: https://sorna--12359.org.readthedocs.build/en/12359/


📚 Documentation preview 📚: https://sorna-ko--12359.org.readthedocs.build/ko/12359/


📚 Documentation preview 📚: https://sorna--12359.org.readthedocs.build/en/12359/


📚 Documentation preview 📚: https://sorna-ko--12359.org.readthedocs.build/ko/12359/

@jopemachine jopemachine force-pushed the feat/BA-6554-app-config-fragments-service branch from b40117b to 870a98b Compare June 23, 2026 02:48
@jopemachine jopemachine force-pushed the feat/BA-6555-app-config-merge-engine branch from 296a9a0 to 07bafd2 Compare June 23, 2026 02:53
@github-actions github-actions Bot added size:L 100~500 LoC comp:manager Related to Manager component labels Jun 23, 2026
@jopemachine jopemachine force-pushed the feat/BA-6554-app-config-fragments-service branch 3 times, most recently from 1777f48 to 9dc1da7 Compare June 23, 2026 03:30
@jopemachine jopemachine force-pushed the feat/BA-6555-app-config-merge-engine branch 2 times, most recently from 47878cb to ae7f25f Compare June 23, 2026 03:34
@jopemachine jopemachine force-pushed the feat/BA-6554-app-config-fragments-service branch from 9dc1da7 to 59a0a70 Compare June 23, 2026 03:38
@jopemachine jopemachine force-pushed the feat/BA-6555-app-config-merge-engine branch 2 times, most recently from e9f0e1b to 684601c Compare June 23, 2026 03:44
@jopemachine jopemachine force-pushed the feat/BA-6554-app-config-fragments-service branch from 59a0a70 to 7694f19 Compare June 23, 2026 03:48
@jopemachine jopemachine force-pushed the feat/BA-6555-app-config-merge-engine branch from 684601c to e5986d3 Compare June 23, 2026 03:49
@jopemachine jopemachine force-pushed the feat/BA-6554-app-config-fragments-service branch from 7694f19 to 02106ca Compare June 23, 2026 04:04
@jopemachine jopemachine force-pushed the feat/BA-6555-app-config-merge-engine branch from e5986d3 to cb34649 Compare June 23, 2026 06:47
@github-actions github-actions Bot added size:XL 500~ LoC and removed size:L 100~500 LoC labels Jun 23, 2026
@jopemachine jopemachine changed the title feat(BA-6555): add app_config merge engine (deep-merge across applicable fragments) feat(BA-6555): add app_config service layer Jun 23, 2026
Comment thread src/ai/backend/manager/services/app_config/merge.py Outdated
@jopemachine jopemachine force-pushed the feat/BA-6555-app-config-merge-engine branch from 94e2ca4 to 08e919c Compare June 23, 2026 07:17
@github-actions github-actions Bot added size:L 100~500 LoC and removed size:XL 500~ LoC labels Jun 23, 2026
@jopemachine jopemachine force-pushed the feat/BA-6555-app-config-merge-engine branch from fb7af85 to 4b9ce45 Compare June 23, 2026 09:07
Comment thread src/ai/backend/manager/repositories/app_config_fragment/types.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR (BA-6555) adds the read side of the AppConfig stack under BEP-1052: a service that resolves the merged AppConfig for a (user, config_name) principal by deep-merging the visible public / domain / user fragments in allow-list rank order. It is the read foundation for the upcoming REST v2 API (#12377) and introduces no HTTP surface itself.

Changes:

  • New AppConfigService with resolve / resolve_bulk actions, processors, and a _merge_configs helper built on common.utils.deep_merge.
  • New repository/db_source methods list_visible_fragments / list_visible_fragments_bulk (single OR-filtered query joined to the allow-list for rank), plus per-scope visibility conditions and the AppConfigScopeArguments value object.
  • New AppConfigData merged-view type and unit/real-DB tests.

Reviewed changes

Copilot reviewed 14 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
services/app_config/service.py Merge engine (_merge_configs) and resolve / resolve_bulk service methods.
services/app_config/actions/{base,resolve,resolve_bulk}.py USER-scope GET actions/results carrying merged AppConfigData.
services/app_config/processors.py Processor package wiring the two resolve actions.
data/app_config/types.py AppConfigData merged-view dataclass.
repositories/app_config_fragment/repository.py Delegates the two visible-fragment reads to the db_source.
repositories/app_config_fragment/db_source/db_source.py Builds the joined, rank-ordered visible-fragments query.
repositories/app_config_fragment/types.py Adds AppConfigScopeArguments.
models/app_config_fragment/conditions.py by_public/domain/user_visibility conditions.
tests/.../services/app_config/*, tests/.../repositories/app_config_fragment/test_repository.py Service + real-DB repository tests.
changes/12359.feature.md Changelog fragment.

Key points raised: the deep_merge list-merge behavior diverges from the described "wholesale-replace" semantics (and is untested), the config empty-case returns {} rather than the None the description claims, and the "single query" read path actually issues a discarded extra COUNT(*).


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ai/backend/manager/services/app_config/service.py Outdated
Comment thread src/ai/backend/manager/data/app_config/types.py Outdated
Comment thread tests/unit/manager/services/app_config/test_service.py Outdated
Comment thread src/ai/backend/manager/services/app_config/service.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 13 changed files in this pull request and generated 3 comments.

Comment thread src/ai/backend/manager/services/app_config/service.py
Comment thread src/ai/backend/manager/services/app_config/actions/resolve.py
Comment thread src/ai/backend/manager/services/app_config/actions/resolve_bulk.py
"""

config_names: list[str]
scope: AppConfigScopeArguments | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it assuming that it only handles items within the same scope?

Resolve the merged AppConfig for a (user, config_name) on top of the repository's
visible-fragments query:
- AppConfigData: the merged per-user view (ordered fragments + deep-merged config; None
  when no fragment contributes)
- deep-merge in rank order — nested dicts recurse, lists/scalars are replaced wholesale by
  the higher-rank fragment
- resolve / resolve_bulk actions + processors (bulk repeats duplicate names in order)
- mocked-repository service tests

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Documentations comp:common Related to Common component comp:manager Related to Manager component require:db-migration Automatically set when alembic migrations are added or updated size:XL 500~ LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants