Skip to content

Commit 0e9c598

Browse files
PatStLouisCopilot
andauthored
Feature: Add plugin installer (#3955)
* plugin installer Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * simplify logic Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * linting Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * linting Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * linting Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * fix repo url parsing security issue Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * add unit tests Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * more tests Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * handle plugin version exception Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * linting Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * fix scenario tests Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * formatting Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * Update acapy_agent/utils/plugin_installer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Patrick St-Louis <43082425+PatStLouis@users.noreply.github.com> * Update acapy_agent/utils/plugin_installer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Patrick St-Louis <43082425+PatStLouis@users.noreply.github.com> * Update acapy_agent/utils/plugin_installer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Patrick St-Louis <43082425+PatStLouis@users.noreply.github.com> * Update acapy_agent/utils/plugin_installer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Patrick St-Louis <43082425+PatStLouis@users.noreply.github.com> * Update acapy_agent/utils/plugin_installer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Patrick St-Louis <43082425+PatStLouis@users.noreply.github.com> * Update acapy_agent/utils/plugin_installer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Patrick St-Louis <43082425+PatStLouis@users.noreply.github.com> * Update acapy_agent/utils/plugin_installer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Patrick St-Louis <43082425+PatStLouis@users.noreply.github.com> * Update acapy_agent/utils/tests/test_plugin_installer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Patrick St-Louis <43082425+PatStLouis@users.noreply.github.com> * address pr comments Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * remove http Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> * address sonarqube issues Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> --------- Signed-off-by: Patrick St-Louis <patrick.st-louis@opsecid.ca> Signed-off-by: Patrick St-Louis <43082425+PatStLouis@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent cafca00 commit 0e9c598

6 files changed

Lines changed: 2205 additions & 3 deletions

File tree

acapy_agent/admin/routes.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Admin server routes."""
22

3+
import logging
34
import re
45

56
from aiohttp import web
@@ -12,6 +13,16 @@
1213
from ..version import __version__
1314
from .decorators.auth import admin_authentication
1415

16+
LOGGER = logging.getLogger(__name__)
17+
18+
19+
# Lazy import to avoid import-time issues
20+
def _get_plugin_version(plugin_name: str):
21+
"""Lazy import wrapper for get_plugin_version."""
22+
from ..utils.plugin_installer import get_plugin_version
23+
24+
return get_plugin_version(plugin_name)
25+
1526

1627
class AdminModulesSchema(OpenAPISchema):
1728
"""Schema for the modules endpoint."""
@@ -71,12 +82,35 @@ async def plugins_handler(request: web.BaseRequest):
7182
request: aiohttp request object
7283
7384
Returns:
74-
The module list response
85+
The module list response with plugin names and versions
7586
7687
"""
7788
registry = request.app["context"].inject_or(PluginRegistry)
7889
plugins = registry and sorted(registry.plugin_names) or []
79-
return web.json_response({"result": plugins})
90+
91+
# Get versions for external plugins only (skip built-in acapy_agent plugins)
92+
external_plugins = []
93+
for plugin_name in plugins:
94+
if not plugin_name.startswith("acapy_agent."):
95+
# External plugin - try to get version info
96+
# Wrap in try/except to prevent failures from affecting the endpoint
97+
try:
98+
version_info = _get_plugin_version(plugin_name) or {}
99+
except Exception:
100+
# If version lookup fails, just include plugin without version info
101+
LOGGER.debug(
102+
"Failed to get version info for plugin %s", plugin_name, exc_info=True
103+
)
104+
version_info = {}
105+
external_plugins.append(
106+
{
107+
"name": plugin_name,
108+
"package_version": version_info.get("package_version", None),
109+
"source_version": version_info.get("source_version", None),
110+
}
111+
)
112+
113+
return web.json_response({"result": plugins, "external": external_plugins})
80114

81115

82116
@docs(tags=["server"], summary="Fetch the server configuration")
@@ -95,7 +129,10 @@ async def config_handler(request: web.BaseRequest):
95129
config = {
96130
k: (
97131
request.app["context"].settings[k]
98-
if (isinstance(request.app["context"].settings[k], (str, int)))
132+
if (
133+
isinstance(request.app["context"].settings[k], (str, int))
134+
or request.app["context"].settings[k] is None
135+
)
99136
else request.app["context"].settings[k].copy()
100137
)
101138
for k in request.app["context"].settings

acapy_agent/commands/start.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from ..config.default_context import DefaultContextBuilder
2020
from ..config.util import common_config
2121
from ..core.conductor import Conductor
22+
from ..utils.plugin_installer import install_plugins_from_config
23+
from ..version import __version__ as acapy_version
2224
from . import PROG
2325

2426
LOGGER = logging.getLogger(__name__)
@@ -60,6 +62,40 @@ async def run_app(argv: Sequence[str] = None):
6062
settings = get_settings(args)
6163
common_config(settings)
6264

65+
# Install plugins if auto-install is enabled and plugins are specified
66+
external_plugins = settings.get("external_plugins", [])
67+
if external_plugins:
68+
auto_install = settings.get("auto_install_plugins", False)
69+
plugin_version = settings.get("plugin_install_version")
70+
71+
if auto_install:
72+
version_info = (
73+
f"version {plugin_version}"
74+
if plugin_version
75+
else f"current ACA-Py version ({acapy_version})"
76+
)
77+
LOGGER.info(
78+
"Auto-installing plugins from acapy-plugins repository: %s (%s)",
79+
", ".join(external_plugins),
80+
version_info,
81+
)
82+
83+
failed_plugins = install_plugins_from_config(
84+
plugin_names=external_plugins,
85+
auto_install=auto_install,
86+
plugin_version=plugin_version,
87+
)
88+
89+
if failed_plugins:
90+
LOGGER.error(
91+
"Failed to install the following plugins: %s. "
92+
"Please ensure these plugins are available in the "
93+
"acapy-plugins repository or install them manually before "
94+
"starting ACA-Py.",
95+
", ".join(failed_plugins),
96+
)
97+
sys.exit(1)
98+
6399
# Set ledger to read-only if explicitly specified
64100
settings["ledger.read_only"] = settings.get("read_only_ledger", False)
65101

acapy_agent/config/argparse.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,23 @@ def add_arguments(self, parser: ArgumentParser):
705705
),
706706
)
707707

708+
parser.add_argument(
709+
"--auto-install-plugins",
710+
dest="auto_install_plugins",
711+
nargs="?",
712+
const=True,
713+
default=False,
714+
metavar="<version>",
715+
env_var="ACAPY_AUTO_INSTALL_PLUGINS",
716+
help=(
717+
"Automatically install missing plugins from the "
718+
"acapy-plugins repository. If specified without a value, uses "
719+
"current ACA-Py version. If a version is provided (e.g., 1.3.2), "
720+
"uses that version for plugin installation. "
721+
"Default: false (disabled)."
722+
),
723+
)
724+
708725
parser.add_argument(
709726
"--storage-type",
710727
type=str,
@@ -803,6 +820,26 @@ def get_settings(self, args: Namespace) -> dict:
803820
reduce(lambda v, k: {k: v}, key.split(".")[::-1], value),
804821
)
805822

823+
# Auto-install plugins: can be True (use current version),
824+
# version string (e.g., "1.3.2"), or False
825+
if hasattr(args, "auto_install_plugins"):
826+
auto_install_value = args.auto_install_plugins
827+
if auto_install_value is True:
828+
# Flag present without value - use current ACA-Py version
829+
settings["auto_install_plugins"] = True
830+
settings["plugin_install_version"] = None # Use current version
831+
elif isinstance(auto_install_value, str):
832+
# Flag present with version value
833+
settings["auto_install_plugins"] = True
834+
settings["plugin_install_version"] = auto_install_value
835+
else:
836+
# False or None - disabled
837+
settings["auto_install_plugins"] = False
838+
settings["plugin_install_version"] = None
839+
else:
840+
settings["auto_install_plugins"] = False
841+
settings["plugin_install_version"] = None
842+
806843
if args.storage_type:
807844
settings["storage_type"] = args.storage_type
808845

0 commit comments

Comments
 (0)