Ticket draft: variant-aware service install in plugin resolver flow
Title
Plugins: variant-aware service install in resolver flow (auth[org], database[sqlite], ai[langchain])
Body
Problem
The plugin dependency resolver canonicalizes bracket variants to their
base names. A plugin declaring:
required_services = ["auth[org]"]
resolves to base auth for both the presence check and the registry
lookup (aegis/core/plugins/resolver.py::_resolve_one,
extract_base_component_name(dep_name)). The variant suffix is
discarded before the resolver even queues an install — and even if we
preserved it through the resolver, the install path
(ManualUpdater.add_service) doesn't accept variant info today: it
takes a service name + an optional service_data dict, and doesn't
know how to upgrade an already-enabled service to a higher level.
Behavior matrix (current state):
| Project state |
Plugin needs |
Resolver does |
Install does |
Expected |
| no auth |
auth[org] |
queue base auth |
install bare auth |
install auth at org level |
auth_level: basic |
auth[org] |
skip (auth present) |
nothing |
upgrade to org |
auth_level: org |
auth[org] |
skip (auth present) |
nothing |
nothing ✓ |
Two of three scenarios silently install the wrong thing or do nothing.
Plugin runtime fails on missing tables.
Why this lives in a follow-up ticket
The PR that added the resolver (#776) and the round 2 of Copilot fixes
(this PR) shipped a deliberate canonicalize-to-base trade-off. No
plugin in milestone #17 declares variant deps yet, and a half-fix
(resolver-only) would lie to itself: the resolver would claim to queue
auth[org] while the install step downgraded it back to base. Better
to acknowledge the gap as a known limitation than ship an inconsistent
half. Documented in
aegis/core/plugins/resolver.py::_resolve_one docstring and the F2
"Out of scope" note in the round-2 PR.
Scope of the fix
Three layers need to coordinate. None alone is sufficient:
1. Resolver: preserve variant info
File: aegis/core/plugins/resolver.py
- Stop discarding the bracket suffix from
dep_name. Strip only
version constraints (auth>=1.0 → auth); keep variants
(auth[org] → auth[org]).
- Update
ResolvedDep to carry the raw constraint alongside the
base name + spec, so the install path can decide what variant to
install:
@dataclass(frozen=True)
class ResolvedDep:
name: str # base, e.g. "auth"
variant: str | None # e.g. "org" or None
kind: PluginKind
spec: PluginSpec
- Variant-aware presence check: instead of
_is_present(base, ...)
alone, also compare against the project's installed variant. For
auth this means answers["auth_level"] (string); for ai it's
answers["ai_backend"] / answers["ai_framework"]; for scheduler
it's answers["scheduler_backend"]. Each service has its own
"variant key" — generalize so the resolver doesn't hard-code a
service-by-service map. Suggested: spec-side declaration (e.g.
PluginSpec.variant_answer_key) so each service spec advertises
where its variant lives in answers.
2. Install path: accept and apply variant
File: aegis/core/manual_updater.py
- Extend
ManualUpdater.add_service(name, service_data, ...) so
variant-only deltas get applied through the same upgrade path
add_service_command already uses (the is_auth_upgrade branch in
add_component at line ~167-181 — but generalized to any
service-with-variant). Concretely: when the service is already
enabled and service_data contains the variant key with a
different value than the project's current variant, drive the
upgrade flow instead of erroring with "already enabled".
3. Caller: pass variant to install
File: aegis/commands/add.py::_install_plugin
- For each
ResolvedDep with a non-None variant, build the
service_data dict the upgrade flow needs and pass it through to
add_service. Mirror what add_service_command does today for
bracket-syntax CLI input (see aegis/commands/add_service.py
lines 200-260 for the auth/ai/insights variant-parsing patterns).
Out of scope for this ticket
- Cross-variant conflicts. If the project has
auth[org] and a
new plugin wants auth[basic], that's a downgrade and should
refuse — not silently downgrade. Treat as future work.
- Component variants (
database[sqlite] vs database[postgres]).
Same logic applies but the install side has a separate path
(scheduler_backend selection in add_component). Worth doing in
the same ticket if the test surface is contained, otherwise split.
- Variant-aware version constraints (
auth[org]>=2.0). The
resolver strips constraints first; combining variant + version is a
concern for the version-compat work in #777.
Tests to add
tests/core/test_plugin_resolver.py::TestBracketVariants —
variant mismatch must NOT short-circuit as "already installed".
auth_level: basic + required_services=["auth[org]"] → queued.
tests/core/test_manual_updater_service.py — add_service("auth", {"auth_level": "org"}) against an already-installed basic auth
upgrades successfully, doesn't raise "already enabled".
tests/cli/test_add_plugin_resolver_integration.py — full path:
plugin needs auth[org], project has basic, install completes
with org tables present, plugin's runtime org_invites query
succeeds.
Acceptance criteria
- All three rows in the matrix above produce the Expected column.
- No regressions in the existing canonicalize-to-base scenarios for
plugins that declare base names (no variant).
- The
aegis-plugin-test fixture grows a variant-only test plugin
to lock the new behavior in.
References
- PR introducing forward dep resolution: #776
- PR shipping the canonicalize-to-base trade-off: this branch's PR
(link when filed)
- Existing variant-handling code to mirror:
aegis/commands/add_service.py (option parsing per service)
- Already-enabled-but-upgrading code to generalize:
aegis/core/manual_updater.py::add_component lines 167-181
(is_auth_upgrade branch)
Ticket draft: variant-aware service install in plugin resolver flow
Title
Plugins: variant-aware service install in resolver flow (auth[org], database[sqlite], ai[langchain])Body
Problem
The plugin dependency resolver canonicalizes bracket variants to their
base names. A plugin declaring:
resolves to base
authfor both the presence check and the registrylookup (
aegis/core/plugins/resolver.py::_resolve_one,extract_base_component_name(dep_name)). The variant suffix isdiscarded before the resolver even queues an install — and even if we
preserved it through the resolver, the install path
(
ManualUpdater.add_service) doesn't accept variant info today: ittakes a service name + an optional
service_datadict, and doesn'tknow how to upgrade an already-enabled service to a higher level.
Behavior matrix (current state):
auth[org]authauthauthat org levelauth_level: basicauth[org]orgauth_level: orgauth[org]Two of three scenarios silently install the wrong thing or do nothing.
Plugin runtime fails on missing tables.
Why this lives in a follow-up ticket
The PR that added the resolver (#776) and the round 2 of Copilot fixes
(this PR) shipped a deliberate canonicalize-to-base trade-off. No
plugin in milestone #17 declares variant deps yet, and a half-fix
(resolver-only) would lie to itself: the resolver would claim to queue
auth[org]while the install step downgraded it back to base. Betterto acknowledge the gap as a known limitation than ship an inconsistent
half. Documented in
aegis/core/plugins/resolver.py::_resolve_onedocstring and the F2"Out of scope" note in the round-2 PR.
Scope of the fix
Three layers need to coordinate. None alone is sufficient:
1. Resolver: preserve variant info
File:
aegis/core/plugins/resolver.pydep_name. Strip onlyversion constraints (
auth>=1.0→auth); keep variants(
auth[org]→auth[org]).ResolvedDepto carry the raw constraint alongside thebase name + spec, so the install path can decide what variant to
install:
_is_present(base, ...)alone, also compare against the project's installed variant. For
auth this means
answers["auth_level"](string); for ai it'sanswers["ai_backend"]/answers["ai_framework"]; for schedulerit's
answers["scheduler_backend"]. Each service has its own"variant key" — generalize so the resolver doesn't hard-code a
service-by-service map. Suggested: spec-side declaration (e.g.
PluginSpec.variant_answer_key) so each service spec advertiseswhere its variant lives in
answers.2. Install path: accept and apply variant
File:
aegis/core/manual_updater.pyManualUpdater.add_service(name, service_data, ...)sovariant-only deltas get applied through the same upgrade path
add_service_commandalready uses (theis_auth_upgradebranch inadd_componentat line ~167-181 — but generalized to anyservice-with-variant). Concretely: when the service is already
enabled and
service_datacontains the variant key with adifferent value than the project's current variant, drive the
upgrade flow instead of erroring with "already enabled".
3. Caller: pass variant to install
File:
aegis/commands/add.py::_install_pluginResolvedDepwith a non-Nonevariant, build theservice_datadict the upgrade flow needs and pass it through toadd_service. Mirror whatadd_service_commanddoes today forbracket-syntax CLI input (see
aegis/commands/add_service.pylines 200-260 for the auth/ai/insights variant-parsing patterns).
Out of scope for this ticket
auth[org]and anew plugin wants
auth[basic], that's a downgrade and shouldrefuse — not silently downgrade. Treat as future work.
database[sqlite]vsdatabase[postgres]).Same logic applies but the install side has a separate path
(
scheduler_backendselection inadd_component). Worth doing inthe same ticket if the test surface is contained, otherwise split.
auth[org]>=2.0). Theresolver strips constraints first; combining variant + version is a
concern for the version-compat work in #777.
Tests to add
tests/core/test_plugin_resolver.py::TestBracketVariants—variant mismatch must NOT short-circuit as "already installed".
auth_level: basic+required_services=["auth[org]"]→ queued.tests/core/test_manual_updater_service.py—add_service("auth", {"auth_level": "org"})against an already-installed basic authupgrades successfully, doesn't raise "already enabled".
tests/cli/test_add_plugin_resolver_integration.py— full path:plugin needs
auth[org], project has basic, install completeswith org tables present, plugin's runtime
org_invitesquerysucceeds.
Acceptance criteria
plugins that declare base names (no variant).
aegis-plugin-testfixture grows a variant-only test pluginto lock the new behavior in.
References
(link when filed)
aegis/commands/add_service.py(option parsing per service)aegis/core/manual_updater.py::add_componentlines 167-181(
is_auth_upgradebranch)