talkops-opscloud 0.3.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opscloud/__init__.py +5 -0
- opscloud/__main__.py +6 -0
- opscloud/_constants.py +53 -0
- opscloud/_debug.py +145 -0
- opscloud/_debug_buffer.py +250 -0
- opscloud/_textual_patches.py +367 -0
- opscloud/_version.py +15 -0
- opscloud/agent/__init__.py +27 -0
- opscloud/agent/config.py +168 -0
- opscloud/agent/factory.py +1103 -0
- opscloud/approval_mode.py +44 -0
- opscloud/backend/__init__.py +49 -0
- opscloud/backend/composite.py +89 -0
- opscloud/backend/local.py +183 -0
- opscloud/backend/registry.py +79 -0
- opscloud/backend/sandbox_config.py +107 -0
- opscloud/backend/sandbox_factory.py +432 -0
- opscloud/built_in_skills/remember/SKILL.md +124 -0
- opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
- opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
- opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
- opscloud/cli/__init__.py +1 -0
- opscloud/cli/commands/__init__.py +15 -0
- opscloud/cli/commands/auth.py +45 -0
- opscloud/cli/commands/doctor.py +100 -0
- opscloud/cli/commands/mcp.py +137 -0
- opscloud/cli/commands/threads.py +51 -0
- opscloud/cli/interactive.py +154 -0
- opscloud/cli/main.py +903 -0
- opscloud/cli/non_interactive.py +425 -0
- opscloud/cli/server_manager.py +58 -0
- opscloud/commands/__init__.py +19 -0
- opscloud/commands/_base.py +110 -0
- opscloud/commands/_guards.py +52 -0
- opscloud/commands/_router.py +146 -0
- opscloud/commands/_types.py +36 -0
- opscloud/commands/core/__init__.py +47 -0
- opscloud/commands/core/auth.py +94 -0
- opscloud/commands/core/bug.py +46 -0
- opscloud/commands/core/clear.py +106 -0
- opscloud/commands/core/cloud.py +185 -0
- opscloud/commands/core/compact.py +128 -0
- opscloud/commands/core/config_cmd.py +120 -0
- opscloud/commands/core/context.py +220 -0
- opscloud/commands/core/cost.py +44 -0
- opscloud/commands/core/doctor.py +535 -0
- opscloud/commands/core/effort.py +108 -0
- opscloud/commands/core/exit_cmd.py +41 -0
- opscloud/commands/core/fast.py +148 -0
- opscloud/commands/core/help_cmd.py +94 -0
- opscloud/commands/core/mcp.py +134 -0
- opscloud/commands/core/model.py +109 -0
- opscloud/commands/core/permissions.py +196 -0
- opscloud/commands/core/plugins.py +92 -0
- opscloud/commands/core/pool.py +108 -0
- opscloud/commands/core/resume.py +97 -0
- opscloud/commands/core/skills.py +178 -0
- opscloud/commands/power/__init__.py +51 -0
- opscloud/commands/power/agents.py +98 -0
- opscloud/commands/power/btw.py +85 -0
- opscloud/commands/power/copy.py +97 -0
- opscloud/commands/power/goal.py +548 -0
- opscloud/commands/power/loop.py +295 -0
- opscloud/commands/power/memory.py +287 -0
- opscloud/commands/power/review.py +152 -0
- opscloud/commands/power/rubric.py +287 -0
- opscloud/commands/power/runtime.py +321 -0
- opscloud/commands/power/skill_creator.py +70 -0
- opscloud/commands/power/skill_invoke.py +165 -0
- opscloud/commands/power/tasks.py +209 -0
- opscloud/commands/power/trace.py +117 -0
- opscloud/commands/power/ui_toggles.py +135 -0
- opscloud/commands/power/version.py +52 -0
- opscloud/commands/registry.py +176 -0
- opscloud/config/__init__.py +114 -0
- opscloud/config/adapters/__init__.py +5 -0
- opscloud/config/adapters/postgres.py +776 -0
- opscloud/config/adapters/sqlite.py +769 -0
- opscloud/config/aws.py +248 -0
- opscloud/config/cloud_profiles.py +437 -0
- opscloud/config/env_vars.py +81 -0
- opscloud/config/langsmith.py +319 -0
- opscloud/config/manifest.py +817 -0
- opscloud/config/metadata.py +205 -0
- opscloud/config/paths.py +604 -0
- opscloud/config/plugins.py +130 -0
- opscloud/config/settings.py +834 -0
- opscloud/config/store.py +453 -0
- opscloud/config/store_factory.py +97 -0
- opscloud/config/toml_config.py +562 -0
- opscloud/editor.py +173 -0
- opscloud/exceptions.py +65 -0
- opscloud/file_ops.py +217 -0
- opscloud/hooks/__init__.py +139 -0
- opscloud/hooks/env.py +44 -0
- opscloud/hooks/interrupt.py +121 -0
- opscloud/hooks/models/__init__.py +109 -0
- opscloud/hooks/models/adapters.py +19 -0
- opscloud/hooks/models/config.py +79 -0
- opscloud/hooks/models/domain.py +434 -0
- opscloud/hooks/models/transport.py +37 -0
- opscloud/hooks/tools.py +231 -0
- opscloud/input.py +48 -0
- opscloud/integrations/__init__.py +80 -0
- opscloud/integrations/base.py +153 -0
- opscloud/integrations/content_filter.py +119 -0
- opscloud/integrations/event_bus.py +480 -0
- opscloud/integrations/hooks.py +238 -0
- opscloud/integrations/identity.py +181 -0
- opscloud/integrations/notifications.py +134 -0
- opscloud/integrations/stream_bridge.py +397 -0
- opscloud/integrations/thread_store.py +81 -0
- opscloud/json_types.py +20 -0
- opscloud/mcp/__init__.py +57 -0
- opscloud/mcp/config.py +203 -0
- opscloud/mcp/discovery.py +229 -0
- opscloud/mcp/mcp_info.py +109 -0
- opscloud/mcp/middleware.py +102 -0
- opscloud/mcp/preload.py +505 -0
- opscloud/mcp/raw_config.py +149 -0
- opscloud/mcp/semantic_profiler.py +305 -0
- opscloud/mcp/session_manager.py +727 -0
- opscloud/mcp/trust.py +105 -0
- opscloud/memory/__init__.py +27 -0
- opscloud/memory/branch.py +61 -0
- opscloud/memory/guard.py +19 -0
- opscloud/memory/onboarding.py +120 -0
- opscloud/memory/registry.py +134 -0
- opscloud/memory/store.py +223 -0
- opscloud/middleware/__init__.py +106 -0
- opscloud/middleware/_repository_bounds.py +459 -0
- opscloud/middleware/ask_user.py +152 -0
- opscloud/middleware/auto_mode.py +60 -0
- opscloud/middleware/auto_mode_hitl.py +2434 -0
- opscloud/middleware/compaction.py +566 -0
- opscloud/middleware/configurable_model.py +233 -0
- opscloud/middleware/cost_tracking.py +1021 -0
- opscloud/middleware/glm_stall_recovery.py +109 -0
- opscloud/middleware/goal_criteria.py +1234 -0
- opscloud/middleware/goal_state_notice.py +712 -0
- opscloud/middleware/goal_tools.py +407 -0
- opscloud/middleware/headless_mcp_guard.py +88 -0
- opscloud/middleware/jev_model_router.py +1018 -0
- opscloud/middleware/local_context.py +460 -0
- opscloud/middleware/mcp_context.py +151 -0
- opscloud/middleware/mcp_middleware.py +146 -0
- opscloud/middleware/memory_guard.py +393 -0
- opscloud/middleware/model_retry.py +205 -0
- opscloud/middleware/registry.py +137 -0
- opscloud/middleware/reliable_rubric.py +709 -0
- opscloud/middleware/resume_state.py +91 -0
- opscloud/middleware/server_hooks.py +1189 -0
- opscloud/middleware/shell_allow_list.py +90 -0
- opscloud/middleware/skills.py +382 -0
- opscloud/middleware/subagent_artifacts.py +84 -0
- opscloud/middleware/subagent_telemetry.py +261 -0
- opscloud/middleware/subagents.py +243 -0
- opscloud/middleware/tool_filter.py +384 -0
- opscloud/middleware/unified_system_message.py +62 -0
- opscloud/model/__init__.py +37 -0
- opscloud/model/config.py +2992 -0
- opscloud/model/factory.py +477 -0
- opscloud/model/pool.py +459 -0
- opscloud/model/reasoning.py +282 -0
- opscloud/offload.py +173 -0
- opscloud/output.py +64 -0
- opscloud/plugins/__init__.py +189 -0
- opscloud/plugins/_json.py +22 -0
- opscloud/plugins/adapters/__init__.py +13 -0
- opscloud/plugins/adapters/agents.py +112 -0
- opscloud/plugins/adapters/commands.py +190 -0
- opscloud/plugins/adapters/mcp.py +363 -0
- opscloud/plugins/adapters/skills.py +132 -0
- opscloud/plugins/commands_cli.py +541 -0
- opscloud/plugins/discovery.py +512 -0
- opscloud/plugins/manifest.py +335 -0
- opscloud/plugins/marketplace.py +893 -0
- opscloud/plugins/models.py +334 -0
- opscloud/plugins/project_plugins.py +317 -0
- opscloud/plugins/protocol.py +31 -0
- opscloud/plugins/store.py +905 -0
- opscloud/plugins/substitution.py +122 -0
- opscloud/project_utils.py +145 -0
- opscloud/prompts/__init__.py +37 -0
- opscloud/prompts/system.py +483 -0
- opscloud/prompts/templates/system_prompt.md +166 -0
- opscloud/prompts/types.py +54 -0
- opscloud/rubrics/__init__.py +42 -0
- opscloud/rubrics/evaluator.py +404 -0
- opscloud/rubrics/evidence_extractor.py +229 -0
- opscloud/rubrics/generator.py +239 -0
- opscloud/rubrics/jev_compiler.py +175 -0
- opscloud/rubrics/jev_grader.py +297 -0
- opscloud/rubrics/middleware.py +8 -0
- opscloud/schema/__init__.py +107 -0
- opscloud/schema/interrupts.py +981 -0
- opscloud/security/__init__.py +91 -0
- opscloud/security/approval_mode.py +528 -0
- opscloud/security/approval_mode_source.py +230 -0
- opscloud/security/cli_ast_evaluator.py +552 -0
- opscloud/security/jev_classifier.py +651 -0
- opscloud/security/shell_safety.py +340 -0
- opscloud/security/unicode_security.py +439 -0
- opscloud/security/url_validation.py +160 -0
- opscloud/server/__init__.py +13 -0
- opscloud/server/_server_config.py +158 -0
- opscloud/server/server.py +242 -0
- opscloud/server/server_graph.py +93 -0
- opscloud/skills/__init__.py +91 -0
- opscloud/skills/commands.py +1099 -0
- opscloud/skills/invocation.py +200 -0
- opscloud/skills/load.py +388 -0
- opscloud/skills/loader.py +23 -0
- opscloud/skills/merge.py +65 -0
- opscloud/skills/registry.py +263 -0
- opscloud/skills/sources.py +174 -0
- opscloud/skills/trust.py +417 -0
- opscloud/state/__init__.py +142 -0
- opscloud/state/base.py +154 -0
- opscloud/state/cloud_context.py +262 -0
- opscloud/state/goal_channels.py +124 -0
- opscloud/state/goal_state_limits.py +175 -0
- opscloud/state/resume_state.py +58 -0
- opscloud/state/service_context.py +281 -0
- opscloud/state/session.py +957 -0
- opscloud/state/state_migration.py +86 -0
- opscloud/subagents/__init__.py +30 -0
- opscloud/subagents/loader.py +331 -0
- opscloud/subagents/subagents_parser.py +117 -0
- opscloud/subagents/types.py +25 -0
- opscloud/tools/__init__.py +36 -0
- opscloud/tools/catalog.py +37 -0
- opscloud/tools/display.py +24 -0
- opscloud/tools/fetch_url.py +176 -0
- opscloud/tools/goal_tools.py +423 -0
- opscloud/tools/registry.py +105 -0
- opscloud/tools/thread.py +32 -0
- opscloud/tools/web_search.py +215 -0
- opscloud/ui/__init__.py +119 -0
- opscloud/ui/app.py +5400 -0
- opscloud/ui/app.tcss +479 -0
- opscloud/ui/clipboard.py +147 -0
- opscloud/ui/command_registry.py +482 -0
- opscloud/ui/message_store.py +152 -0
- opscloud/ui/permission_store.py +360 -0
- opscloud/ui/preferences.py +113 -0
- opscloud/ui/prompt_manager.py +96 -0
- opscloud/ui/remote_client.py +505 -0
- opscloud/ui/textual_adapter.py +1269 -0
- opscloud/ui/theme.py +728 -0
- opscloud/ui/ui_help.py +415 -0
- opscloud/ui/widgets/__init__.py +383 -0
- opscloud/ui/widgets/_ask_user_types.py +81 -0
- opscloud/ui/widgets/_copy_spans.py +52 -0
- opscloud/ui/widgets/_inline_prompt.py +290 -0
- opscloud/ui/widgets/_js_eval_display.py +139 -0
- opscloud/ui/widgets/_links.py +175 -0
- opscloud/ui/widgets/_tool_stream.py +308 -0
- opscloud/ui/widgets/agent_selector.py +297 -0
- opscloud/ui/widgets/approval.py +741 -0
- opscloud/ui/widgets/ask_user.py +539 -0
- opscloud/ui/widgets/auth.py +695 -0
- opscloud/ui/widgets/auth_manager.py +214 -0
- opscloud/ui/widgets/auto_mode_notice.py +146 -0
- opscloud/ui/widgets/autocomplete.py +709 -0
- opscloud/ui/widgets/chat_input.py +805 -0
- opscloud/ui/widgets/cloud_selector.py +528 -0
- opscloud/ui/widgets/config_manager.py +652 -0
- opscloud/ui/widgets/debug_console.py +1269 -0
- opscloud/ui/widgets/devops_renderers.py +184 -0
- opscloud/ui/widgets/diff.py +237 -0
- opscloud/ui/widgets/effort_selector.py +166 -0
- opscloud/ui/widgets/goal_review.py +612 -0
- opscloud/ui/widgets/goal_status.py +57 -0
- opscloud/ui/widgets/infra_panel.py +141 -0
- opscloud/ui/widgets/install_confirm.py +114 -0
- opscloud/ui/widgets/loading.py +170 -0
- opscloud/ui/widgets/mcp_viewer.py +1204 -0
- opscloud/ui/widgets/messages.py +4461 -0
- opscloud/ui/widgets/model_selector.py +863 -0
- opscloud/ui/widgets/notification_center.py +108 -0
- opscloud/ui/widgets/notification_settings.py +119 -0
- opscloud/ui/widgets/operation_card.py +106 -0
- opscloud/ui/widgets/permissions_manager.py +667 -0
- opscloud/ui/widgets/plugin_manager.py +1212 -0
- opscloud/ui/widgets/pool_selector.py +376 -0
- opscloud/ui/widgets/preamble.py +53 -0
- opscloud/ui/widgets/skills_viewer.py +432 -0
- opscloud/ui/widgets/status.py +1037 -0
- opscloud/ui/widgets/subagent_panel.py +927 -0
- opscloud/ui/widgets/theme_selector.py +232 -0
- opscloud/ui/widgets/thread_selector.py +358 -0
- opscloud/ui/widgets/toast.py +31 -0
- opscloud/ui/widgets/tool_display.py +325 -0
- opscloud/ui/widgets/tool_renderers.py +234 -0
- opscloud/ui/widgets/tool_widgets.py +278 -0
- opscloud/ui/widgets/welcome.py +421 -0
- opscloud/ui/widgets/welcome_popup.py +102 -0
- opscloud/utils/__init__.py +24 -0
- opscloud/utils/cost_estimation.py +169 -0
- opscloud/utils/git.py +390 -0
- opscloud/utils/logger.py +571 -0
- opscloud/utils/session_stats.py +115 -0
- opscloud/utils/startup_error.py +37 -0
- talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
- talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
- talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
- talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
- talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Data models for plugins and marketplaces in K8s Autopilot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
JsonObject = dict[str, Any]
|
|
10
|
+
JsonValue = Any
|
|
11
|
+
|
|
12
|
+
MarketplaceSourceType = Literal["directory", "file", "github", "git", "url"]
|
|
13
|
+
InstallScope = Literal["global", "user", "team", "project", "local"]
|
|
14
|
+
ExternalPluginRepositorySourceType = Literal["github", "git-subdir", "url"]
|
|
15
|
+
UnsupportedComponent = Literal["hooks"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
19
|
+
class LocalMarketplaceSource:
|
|
20
|
+
"""Local directory or JSON file used as a marketplace source."""
|
|
21
|
+
|
|
22
|
+
source_type: Literal["directory", "file"]
|
|
23
|
+
value: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
27
|
+
class RepositoryMarketplaceSource:
|
|
28
|
+
"""GitHub or Git repository used as a marketplace source."""
|
|
29
|
+
|
|
30
|
+
source_type: Literal["github", "git"]
|
|
31
|
+
value: str
|
|
32
|
+
ref: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
36
|
+
class UrlMarketplaceSource:
|
|
37
|
+
"""Marketplace manifest downloaded from an HTTP URL."""
|
|
38
|
+
|
|
39
|
+
source_type: Literal["url"]
|
|
40
|
+
value: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
MarketplaceSource = LocalMarketplaceSource | RepositoryMarketplaceSource | UrlMarketplaceSource
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
47
|
+
class PluginManifest:
|
|
48
|
+
"""Parsed plugin manifest."""
|
|
49
|
+
|
|
50
|
+
name: str | None
|
|
51
|
+
version: str | None = "1.0.0"
|
|
52
|
+
display_name: str | None = None
|
|
53
|
+
description: str | None = None
|
|
54
|
+
author: str | JsonObject | None = None
|
|
55
|
+
component_paths: dict[str, tuple[Path, ...]] = () # type: ignore[assignment]
|
|
56
|
+
inline_mcp: JsonObject = () # type: ignore[assignment]
|
|
57
|
+
agents_dir: Path | None = None
|
|
58
|
+
skills_dir: Path | None = None
|
|
59
|
+
mcp_config: JsonObject = () # type: ignore[assignment]
|
|
60
|
+
hooks_config: JsonObject = () # type: ignore[assignment]
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_dict(cls, data: dict[str, Any]) -> PluginManifest:
|
|
64
|
+
"""Create a PluginManifest from a dictionary."""
|
|
65
|
+
return cls(
|
|
66
|
+
name=data.get("name"),
|
|
67
|
+
version=data.get("version", "1.0.0"),
|
|
68
|
+
display_name=data.get("display_name"),
|
|
69
|
+
description=data.get("description"),
|
|
70
|
+
author=data.get("author"),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
75
|
+
class ComponentInventory:
|
|
76
|
+
"""Inventory of supported plugin components."""
|
|
77
|
+
|
|
78
|
+
skills: tuple[Path, ...] = ()
|
|
79
|
+
mcp_files: tuple[Path, ...] = ()
|
|
80
|
+
agents: tuple[Path, ...] = ()
|
|
81
|
+
commands: tuple[Path, ...] = ()
|
|
82
|
+
unsupported: tuple[UnsupportedComponent, ...] = ()
|
|
83
|
+
warnings: tuple[str, ...] = ()
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def mcp(self) -> tuple[Path, ...]:
|
|
87
|
+
return self.mcp_files
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, kw_only=True)
|
|
91
|
+
class PluginInstance:
|
|
92
|
+
"""A discovered plugin ready to feed adapters and agent context.
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
plugin_id: Stable id in `{name}@{marketplace}` form.
|
|
96
|
+
name: Plugin namespace name.
|
|
97
|
+
marketplace: Parent marketplace used for identity and namespacing.
|
|
98
|
+
version: Version declared by the plugin manifest, if any.
|
|
99
|
+
root: Plugin root directory.
|
|
100
|
+
data_dir: Writable data directory for this plugin.
|
|
101
|
+
manifest: Parsed manifest, if any.
|
|
102
|
+
inventory: Component inventory.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
plugin_id: str = ""
|
|
106
|
+
name: str = ""
|
|
107
|
+
marketplace: str = "default"
|
|
108
|
+
version: str | None = None
|
|
109
|
+
root: Path = Path(".")
|
|
110
|
+
data_dir: Path | None = None
|
|
111
|
+
manifest: PluginManifest | None = None
|
|
112
|
+
inventory: ComponentInventory = ComponentInventory()
|
|
113
|
+
enabled: bool = True
|
|
114
|
+
source: InstallScope = "global"
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
plugin_id: str = "",
|
|
120
|
+
name: str = "",
|
|
121
|
+
marketplace: str = "default",
|
|
122
|
+
version: str | None = None,
|
|
123
|
+
root: Path | None = None,
|
|
124
|
+
root_dir: Path | None = None,
|
|
125
|
+
data_dir: Path | None = None,
|
|
126
|
+
manifest: PluginManifest | None = None,
|
|
127
|
+
inventory: ComponentInventory | None = None,
|
|
128
|
+
enabled: bool = True,
|
|
129
|
+
source: InstallScope = "global",
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Initialize PluginInstance with optional legacy alias support.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
plugin_id: Identifier of the plugin.
|
|
135
|
+
name: Human-readable plugin name.
|
|
136
|
+
marketplace: Marketplace source identifier.
|
|
137
|
+
version: Installed plugin version string.
|
|
138
|
+
root: Root path of the plugin directory.
|
|
139
|
+
root_dir: Alias for root directory path.
|
|
140
|
+
data_dir: Plugin data storage path.
|
|
141
|
+
manifest: Parsed plugin manifest.
|
|
142
|
+
inventory: Inventory of plugin components.
|
|
143
|
+
enabled: Whether plugin is enabled.
|
|
144
|
+
source: Installation scope ("global", "user", "project").
|
|
145
|
+
"""
|
|
146
|
+
effective_root = root or root_dir or Path(".")
|
|
147
|
+
effective_name = name or (manifest.name if manifest and manifest.name else "")
|
|
148
|
+
effective_version = version or (manifest.version if manifest and manifest.version else "1.0.0")
|
|
149
|
+
effective_id = plugin_id or (f"{effective_name}@{marketplace}" if effective_name else "")
|
|
150
|
+
|
|
151
|
+
if effective_id and "@" in effective_id:
|
|
152
|
+
expected = f"{effective_name}@{marketplace}"
|
|
153
|
+
if effective_id != expected:
|
|
154
|
+
msg = f"Plugin id {effective_id!r} does not match expected {expected!r}"
|
|
155
|
+
raise ValueError(msg)
|
|
156
|
+
|
|
157
|
+
object.__setattr__(self, "plugin_id", effective_id)
|
|
158
|
+
object.__setattr__(self, "name", effective_name)
|
|
159
|
+
object.__setattr__(self, "marketplace", marketplace)
|
|
160
|
+
object.__setattr__(self, "version", effective_version)
|
|
161
|
+
object.__setattr__(self, "root", effective_root)
|
|
162
|
+
object.__setattr__(self, "data_dir", data_dir)
|
|
163
|
+
object.__setattr__(self, "manifest", manifest)
|
|
164
|
+
object.__setattr__(self, "inventory", inventory or ComponentInventory())
|
|
165
|
+
object.__setattr__(self, "enabled", enabled)
|
|
166
|
+
object.__setattr__(self, "source", source)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def root_dir(self) -> Path:
|
|
170
|
+
"""Alias for root for backward compatibility."""
|
|
171
|
+
return self.root
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def is_agent_plugin(self) -> bool:
|
|
175
|
+
"""Return True if this plugin provides autonomous agents (agent-based plugin)."""
|
|
176
|
+
if self.inventory.agents:
|
|
177
|
+
return any(
|
|
178
|
+
(p.is_dir() and any(p.glob("*.md"))) or (p.is_file() and p.suffix == ".md")
|
|
179
|
+
for p in self.inventory.agents
|
|
180
|
+
if p.exists()
|
|
181
|
+
)
|
|
182
|
+
if self.root and isinstance(self.root, Path):
|
|
183
|
+
agents_dir = self.root / "agents"
|
|
184
|
+
return agents_dir.is_dir() and any(agents_dir.glob("*.md"))
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def has_agents(self) -> bool:
|
|
189
|
+
"""Alias for is_agent_plugin."""
|
|
190
|
+
return self.is_agent_plugin
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def is_agent_plugin_dir(plugin_dir: Path) -> bool:
|
|
194
|
+
"""Return True if a plugin directory contains an agents/ component with markdown definitions."""
|
|
195
|
+
agents_dir = plugin_dir / "agents"
|
|
196
|
+
return agents_dir.is_dir() and any(agents_dir.glob("*.md"))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
200
|
+
class LocalPluginSource:
|
|
201
|
+
"""A plugin stored relative to its marketplace."""
|
|
202
|
+
|
|
203
|
+
source_type: Literal["local"]
|
|
204
|
+
path: str
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
208
|
+
class GithubPluginSource:
|
|
209
|
+
"""A plugin sourced from a GitHub repository."""
|
|
210
|
+
|
|
211
|
+
source_type: Literal["github"]
|
|
212
|
+
repo: str
|
|
213
|
+
ref: str | None = None
|
|
214
|
+
path: str | None = None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
218
|
+
class GitSubdirectoryPluginSource:
|
|
219
|
+
"""A plugin sourced from a subdirectory in a Git repository."""
|
|
220
|
+
|
|
221
|
+
source_type: Literal["git-subdir"]
|
|
222
|
+
url: str
|
|
223
|
+
ref: str | None = None
|
|
224
|
+
path: str | None = None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
228
|
+
class UrlPluginSource:
|
|
229
|
+
"""A plugin sourced from a Git repository URL or archive."""
|
|
230
|
+
|
|
231
|
+
source_type: Literal["url"]
|
|
232
|
+
url: str
|
|
233
|
+
ref: str | None = None
|
|
234
|
+
path: str | None = None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
PluginSource = LocalPluginSource | GithubPluginSource | GitSubdirectoryPluginSource | UrlPluginSource
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
241
|
+
class MarketplacePluginEntry:
|
|
242
|
+
"""A catalog entry from a marketplace manifest."""
|
|
243
|
+
|
|
244
|
+
name: str
|
|
245
|
+
source: PluginSource
|
|
246
|
+
description: str | None = None
|
|
247
|
+
author: str | JsonObject | None = None
|
|
248
|
+
display_name: str | None = None
|
|
249
|
+
version: str | None = "0.1.0"
|
|
250
|
+
category: str | None = None
|
|
251
|
+
keywords: tuple[str, ...] = ()
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def category_label(self) -> str:
|
|
255
|
+
"""Human-readable category label derived generically from declared category."""
|
|
256
|
+
if not self.category:
|
|
257
|
+
return "Plugins"
|
|
258
|
+
return self.category.replace("-", " ").replace("_", " ").title()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
262
|
+
class PluginMarketplace:
|
|
263
|
+
"""A parsed marketplace manifest."""
|
|
264
|
+
|
|
265
|
+
name: str
|
|
266
|
+
root: Path
|
|
267
|
+
manifest_path: Path
|
|
268
|
+
metadata: JsonObject
|
|
269
|
+
plugins: tuple[MarketplacePluginEntry, ...]
|
|
270
|
+
warnings: tuple[str, ...] = ()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
274
|
+
class MarketplaceRecord:
|
|
275
|
+
"""Persisted marketplace source record."""
|
|
276
|
+
|
|
277
|
+
name: str
|
|
278
|
+
source_type: MarketplaceSourceType
|
|
279
|
+
source: str
|
|
280
|
+
install_location: str
|
|
281
|
+
ref: str | None = None
|
|
282
|
+
plugin_count: int = 0
|
|
283
|
+
is_team: bool = False
|
|
284
|
+
is_project: bool = False
|
|
285
|
+
is_default: bool = False
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
289
|
+
class InstalledPluginEntry:
|
|
290
|
+
"""Install record for a plugin in local state."""
|
|
291
|
+
|
|
292
|
+
install_path: str
|
|
293
|
+
version: str | None = None
|
|
294
|
+
scope: InstallScope = "user"
|
|
295
|
+
project_path: str | None = None
|
|
296
|
+
installed_at: str | None = None
|
|
297
|
+
last_updated: str | None = None
|
|
298
|
+
git_commit_sha: str | None = None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
302
|
+
class PluginDiscoveryResult:
|
|
303
|
+
"""Result from plugin discovery."""
|
|
304
|
+
|
|
305
|
+
plugins: tuple[PluginInstance, ...]
|
|
306
|
+
warnings: tuple[str, ...] = ()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def split_plugin_id(plugin_id: str) -> tuple[str, str]:
|
|
310
|
+
"""Split a plugin id in `{plugin}@{marketplace}` form.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Plugin and marketplace names.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
ValueError: If either part is missing.
|
|
317
|
+
"""
|
|
318
|
+
if "@" not in plugin_id:
|
|
319
|
+
msg = f"Invalid plugin id {plugin_id!r}; expected name@marketplace"
|
|
320
|
+
raise ValueError(msg)
|
|
321
|
+
plugin, marketplace = plugin_id.rsplit("@", 1)
|
|
322
|
+
if not plugin or not marketplace:
|
|
323
|
+
msg = f"Invalid plugin id {plugin_id!r}; expected name@marketplace"
|
|
324
|
+
raise ValueError(msg)
|
|
325
|
+
return plugin, marketplace
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def namespaced_skill_name(
|
|
329
|
+
namespace: str,
|
|
330
|
+
name: str,
|
|
331
|
+
subfolders: tuple[str, ...] = (),
|
|
332
|
+
) -> str:
|
|
333
|
+
"""Qualify a skill name under its plugin namespace."""
|
|
334
|
+
return ":".join((namespace, *subfolders, name)).lower()
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Auto-discovery and loading of project-local plugin marketplaces.
|
|
2
|
+
|
|
3
|
+
When a project contains a marketplace manifest inside its ``.opscloud`` directory
|
|
4
|
+
(or project root, e.g. ``.claude-plugin/marketplace.json``), this module automatically
|
|
5
|
+
discovers all declared plugins and bifurcates them into two categories:
|
|
6
|
+
|
|
7
|
+
1. **Agent plugins** — plugins whose source root contains an ``agents/``
|
|
8
|
+
directory. Each ``*.md`` file under ``agents/`` becomes a subagent
|
|
9
|
+
definition whose skills, MCP servers, and commands are bound *exclusively*
|
|
10
|
+
to that subagent (they are NOT exposed on the main agent).
|
|
11
|
+
|
|
12
|
+
2. **Non-agent plugins** — plugins without an ``agents/`` directory. Their
|
|
13
|
+
skills, MCP servers, and commands are bound to the **main** agent
|
|
14
|
+
via the standard middleware/session-manager pipeline.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
from opscloud.plugins.manifest import build_inventory, load_manifest
|
|
25
|
+
from opscloud.plugins.marketplace import (
|
|
26
|
+
MarketplaceError,
|
|
27
|
+
_load_marketplace_file,
|
|
28
|
+
find_marketplace_manifest,
|
|
29
|
+
materialize_plugin_source,
|
|
30
|
+
)
|
|
31
|
+
from opscloud.plugins.models import (
|
|
32
|
+
ComponentInventory,
|
|
33
|
+
PluginInstance,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from opscloud.utils.logger import get_logger
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
from opscloud.plugins.adapters.skills import PluginSkillSource
|
|
40
|
+
from opscloud.subagents.types import SubagentMetadata
|
|
41
|
+
|
|
42
|
+
logger = get_logger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ProjectPluginResult:
|
|
47
|
+
"""Aggregated result of auto-loading project-local plugins."""
|
|
48
|
+
|
|
49
|
+
subagent_metas: list[SubagentMetadata] = field(default_factory=list)
|
|
50
|
+
"""Subagent metadata from agent plugins (bound to subagents only)."""
|
|
51
|
+
|
|
52
|
+
main_skill_sources: list[PluginSkillSource] = field(default_factory=list)
|
|
53
|
+
"""Skill source tuples for non-agent plugins (bound to main agent)."""
|
|
54
|
+
|
|
55
|
+
main_mcp_configs: list[dict[str, Any]] = field(default_factory=list)
|
|
56
|
+
"""MCP server configs from non-agent plugins (bound to main agent)."""
|
|
57
|
+
|
|
58
|
+
main_commands: list[Path] = field(default_factory=list)
|
|
59
|
+
"""Command paths from non-agent plugins (bound to main agent)."""
|
|
60
|
+
|
|
61
|
+
warnings: list[str] = field(default_factory=list)
|
|
62
|
+
"""Non-fatal warnings encountered during discovery."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _has_agents(inventory: ComponentInventory) -> bool:
|
|
66
|
+
"""Return True if the inventory contains at least one populated agents dir or file."""
|
|
67
|
+
for path in inventory.agents:
|
|
68
|
+
try:
|
|
69
|
+
if path.is_dir() and any(path.iterdir()):
|
|
70
|
+
return True
|
|
71
|
+
if path.is_file() and path.suffix == ".md":
|
|
72
|
+
return True
|
|
73
|
+
except OSError:
|
|
74
|
+
continue
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _collect_skill_names(inventory: ComponentInventory) -> list[str]:
|
|
79
|
+
"""Derive skill names from the inventory's skill paths.
|
|
80
|
+
|
|
81
|
+
Convention: each ``skills/<name>/SKILL.md`` yields skill name ``<name>``.
|
|
82
|
+
A bare ``SKILL.md`` at the plugin root yields the parent folder name.
|
|
83
|
+
"""
|
|
84
|
+
names: list[str] = []
|
|
85
|
+
for path in inventory.skills:
|
|
86
|
+
try:
|
|
87
|
+
if path.is_dir():
|
|
88
|
+
for child in sorted(path.iterdir()):
|
|
89
|
+
if child.is_dir() and (child / "SKILL.md").is_file():
|
|
90
|
+
names.append(child.name)
|
|
91
|
+
elif path.name == "SKILL.md":
|
|
92
|
+
names.append(path.parent.name)
|
|
93
|
+
except OSError:
|
|
94
|
+
continue
|
|
95
|
+
return names
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _read_mcp_config(mcp_files: tuple[Path, ...]) -> dict[str, Any] | None:
|
|
99
|
+
"""Read and merge MCP server configs from discovered ``.mcp.json`` files."""
|
|
100
|
+
combined_servers: dict[str, Any] = {}
|
|
101
|
+
for mcp_path in mcp_files:
|
|
102
|
+
try:
|
|
103
|
+
if not mcp_path.is_file():
|
|
104
|
+
continue
|
|
105
|
+
content = mcp_path.read_text(encoding="utf-8")
|
|
106
|
+
try:
|
|
107
|
+
raw = json.loads(content)
|
|
108
|
+
except json.JSONDecodeError:
|
|
109
|
+
from opscloud.plugins.adapters.mcp import _try_repair_mcp_json
|
|
110
|
+
|
|
111
|
+
raw = _try_repair_mcp_json(content)
|
|
112
|
+
if raw is None:
|
|
113
|
+
raise
|
|
114
|
+
servers = raw.get("mcpServers") or raw.get("mcp_servers") or raw
|
|
115
|
+
if isinstance(servers, dict):
|
|
116
|
+
combined_servers.update(servers)
|
|
117
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
118
|
+
logger.warning("Could not parse MCP config %s: %s", mcp_path, exc)
|
|
119
|
+
return {"mcpServers": combined_servers} if combined_servers else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _build_plugin_instance(
|
|
123
|
+
plugin_name: str,
|
|
124
|
+
marketplace_name: str,
|
|
125
|
+
source_root: Path,
|
|
126
|
+
fallback_name: str,
|
|
127
|
+
) -> tuple[PluginInstance | None, list[str]]:
|
|
128
|
+
"""Build a ``PluginInstance`` from a resolved source root."""
|
|
129
|
+
warnings: list[str] = []
|
|
130
|
+
try:
|
|
131
|
+
manifest, _manifest_path, manifest_warnings = load_manifest(
|
|
132
|
+
source_root, fallback_name=fallback_name
|
|
133
|
+
)
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
return None, [f"Skipping plugin {plugin_name}: {exc}"]
|
|
136
|
+
warnings.extend(manifest_warnings)
|
|
137
|
+
|
|
138
|
+
name = manifest.name if manifest and manifest.name else fallback_name
|
|
139
|
+
inventory = build_inventory(source_root, manifest, tuple(warnings))
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
from opscloud.plugins.store import plugin_data_dir
|
|
143
|
+
|
|
144
|
+
instance = PluginInstance(
|
|
145
|
+
plugin_id=f"{plugin_name}@{marketplace_name}",
|
|
146
|
+
name=name,
|
|
147
|
+
marketplace=marketplace_name,
|
|
148
|
+
version=manifest.version if manifest is not None else None,
|
|
149
|
+
root=source_root,
|
|
150
|
+
data_dir=plugin_data_dir(f"{plugin_name}@{marketplace_name}"),
|
|
151
|
+
manifest=manifest,
|
|
152
|
+
inventory=inventory,
|
|
153
|
+
)
|
|
154
|
+
except (ValueError, OSError) as exc:
|
|
155
|
+
return None, [f"Skipping plugin {plugin_name}: {exc}"]
|
|
156
|
+
return instance, list(inventory.warnings)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _process_agent_plugin(
|
|
160
|
+
plugin: PluginInstance,
|
|
161
|
+
) -> tuple[list[SubagentMetadata], list[str]]:
|
|
162
|
+
"""Process an agent plugin: parse subagents and bind skills + MCP to each."""
|
|
163
|
+
from opscloud.plugins.adapters.agents import _enrich_plugin_subagent
|
|
164
|
+
from opscloud.subagents.loader import parse_subagent_file
|
|
165
|
+
|
|
166
|
+
subagents: list[SubagentMetadata] = []
|
|
167
|
+
warnings: list[str] = []
|
|
168
|
+
|
|
169
|
+
skill_names = _collect_skill_names(plugin.inventory)
|
|
170
|
+
mcp_file_paths = [str(p) for p in plugin.inventory.mcp_files if p.is_file()]
|
|
171
|
+
|
|
172
|
+
for agent_path in plugin.inventory.agents:
|
|
173
|
+
try:
|
|
174
|
+
if not agent_path.exists():
|
|
175
|
+
continue
|
|
176
|
+
except OSError:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
md_files: list[tuple[Path, str]] = []
|
|
180
|
+
if agent_path.is_dir():
|
|
181
|
+
for entry in sorted(agent_path.iterdir()):
|
|
182
|
+
if entry.is_file() and entry.suffix == ".md":
|
|
183
|
+
md_files.append((entry, entry.stem))
|
|
184
|
+
elif entry.is_dir():
|
|
185
|
+
agents_md = entry / "AGENTS.md"
|
|
186
|
+
if agents_md.is_file():
|
|
187
|
+
md_files.append((agents_md, entry.name))
|
|
188
|
+
elif agent_path.is_file() and agent_path.suffix == ".md":
|
|
189
|
+
md_files.append((agent_path, agent_path.stem))
|
|
190
|
+
|
|
191
|
+
for md_path, fallback_name in md_files:
|
|
192
|
+
meta = parse_subagent_file(md_path, fallback_name=fallback_name)
|
|
193
|
+
if meta is None:
|
|
194
|
+
warnings.append(f"Could not parse agent definition: {md_path}")
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
if skill_names and not meta.get("skills"):
|
|
198
|
+
meta["skills"] = list(skill_names)
|
|
199
|
+
|
|
200
|
+
if mcp_file_paths and not meta.get("mcp_files"):
|
|
201
|
+
meta["mcp_files"] = list(mcp_file_paths)
|
|
202
|
+
|
|
203
|
+
subagents.append(_enrich_plugin_subagent(meta, plugin))
|
|
204
|
+
|
|
205
|
+
return subagents, warnings
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _process_main_plugin(
|
|
209
|
+
plugin: PluginInstance,
|
|
210
|
+
) -> tuple[list[tuple[str, str, str]], list[dict[str, Any]], list[Path], list[str]]:
|
|
211
|
+
"""Process a non-agent plugin: yield skills, MCP, and commands for main agent."""
|
|
212
|
+
from opscloud.plugins.adapters.skills import plugin_skill_sources
|
|
213
|
+
|
|
214
|
+
warnings: list[str] = []
|
|
215
|
+
skill_sources = plugin_skill_sources((plugin,))
|
|
216
|
+
|
|
217
|
+
mcp_configs: list[dict[str, Any]] = []
|
|
218
|
+
mcp_config = _read_mcp_config(plugin.inventory.mcp_files)
|
|
219
|
+
if mcp_config:
|
|
220
|
+
mcp_configs.append(mcp_config)
|
|
221
|
+
if plugin.manifest and plugin.manifest.inline_mcp:
|
|
222
|
+
mcp_configs.append({"mcpServers": plugin.manifest.inline_mcp})
|
|
223
|
+
|
|
224
|
+
command_paths: list[Path] = list(plugin.inventory.commands)
|
|
225
|
+
|
|
226
|
+
return skill_sources, mcp_configs, command_paths, warnings
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def load_project_plugins(project_root: Path | str | None) -> ProjectPluginResult:
|
|
230
|
+
"""Auto-discover and load all plugins from a project's marketplace manifest.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
project_root: Path to the project root directory.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
A ``ProjectPluginResult`` with bifurcated agent and non-agent plugin data.
|
|
237
|
+
"""
|
|
238
|
+
result = ProjectPluginResult()
|
|
239
|
+
if project_root is None:
|
|
240
|
+
return result
|
|
241
|
+
|
|
242
|
+
root = Path(project_root)
|
|
243
|
+
if not root.is_dir():
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
# Check candidate locations for marketplace manifest
|
|
247
|
+
manifest_path = None
|
|
248
|
+
for candidate in (root / ".opscloud", root / ".opscode", root):
|
|
249
|
+
if candidate.is_dir():
|
|
250
|
+
manifest_path = find_marketplace_manifest(candidate)
|
|
251
|
+
if manifest_path is not None:
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
if manifest_path is None:
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
marketplace = _load_marketplace_file(manifest_path)
|
|
259
|
+
except MarketplaceError as exc:
|
|
260
|
+
result.warnings.append(f"Could not load project marketplace: {exc}")
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
logger.info(
|
|
264
|
+
"Auto-loading project marketplace %r with %d plugin(s)",
|
|
265
|
+
marketplace.name,
|
|
266
|
+
len(marketplace.plugins),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
from opscloud.plugins.store import load_all_disabled_plugin_ids
|
|
270
|
+
|
|
271
|
+
disabled_ids = load_all_disabled_plugin_ids(project_root=root)
|
|
272
|
+
|
|
273
|
+
for entry in marketplace.plugins:
|
|
274
|
+
plugin_id = f"{entry.name}@{marketplace.name}"
|
|
275
|
+
if plugin_id in disabled_ids:
|
|
276
|
+
logger.info("Skipping project plugin %s (explicitly disabled)", plugin_id)
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
source_root = materialize_plugin_source(marketplace, entry)
|
|
280
|
+
if source_root is None:
|
|
281
|
+
result.warnings.append(f"Plugin {entry.name!r}: could not resolve source")
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
plugin, plugin_warnings = _build_plugin_instance(
|
|
285
|
+
plugin_name=entry.name,
|
|
286
|
+
marketplace_name=marketplace.name,
|
|
287
|
+
source_root=source_root,
|
|
288
|
+
fallback_name=entry.name,
|
|
289
|
+
)
|
|
290
|
+
result.warnings.extend(plugin_warnings)
|
|
291
|
+
if plugin is None:
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
if _has_agents(plugin.inventory):
|
|
295
|
+
subagent_metas, agent_warnings = _process_agent_plugin(plugin)
|
|
296
|
+
result.subagent_metas.extend(subagent_metas)
|
|
297
|
+
result.warnings.extend(agent_warnings)
|
|
298
|
+
logger.debug(
|
|
299
|
+
"Plugin %s: agent plugin → %d subagent(s)",
|
|
300
|
+
entry.name,
|
|
301
|
+
len(subagent_metas),
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
skills, mcps, cmds, main_warnings = _process_main_plugin(plugin)
|
|
305
|
+
result.main_skill_sources.extend(skills)
|
|
306
|
+
result.main_mcp_configs.extend(mcps)
|
|
307
|
+
result.main_commands.extend(cmds)
|
|
308
|
+
result.warnings.extend(main_warnings)
|
|
309
|
+
logger.debug(
|
|
310
|
+
"Plugin %s: main plugin → %d skill source(s), %d MCP config(s), %d command(s)",
|
|
311
|
+
entry.name,
|
|
312
|
+
len(skills),
|
|
313
|
+
len(mcps),
|
|
314
|
+
len(cmds),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return result
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Plugin protocol and data models for K8s Autopilot."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from opscloud.plugins.models import (
|
|
6
|
+
ComponentInventory,
|
|
7
|
+
InstalledPluginEntry,
|
|
8
|
+
MarketplacePluginEntry,
|
|
9
|
+
MarketplaceRecord,
|
|
10
|
+
PluginDiscoveryResult,
|
|
11
|
+
PluginInstance,
|
|
12
|
+
PluginManifest,
|
|
13
|
+
PluginMarketplace,
|
|
14
|
+
PluginSource,
|
|
15
|
+
namespaced_skill_name,
|
|
16
|
+
split_plugin_id,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ComponentInventory",
|
|
21
|
+
"InstalledPluginEntry",
|
|
22
|
+
"MarketplacePluginEntry",
|
|
23
|
+
"MarketplaceRecord",
|
|
24
|
+
"PluginDiscoveryResult",
|
|
25
|
+
"PluginInstance",
|
|
26
|
+
"PluginManifest",
|
|
27
|
+
"PluginMarketplace",
|
|
28
|
+
"PluginSource",
|
|
29
|
+
"namespaced_skill_name",
|
|
30
|
+
"split_plugin_id",
|
|
31
|
+
]
|