Skip to content

Plugins: variant-aware service install in resolver flow (auth[org], database[sqlite], ai[langchain]) #671

@lbedner

Description

@lbedner

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.0auth); 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.pyadd_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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions