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.
Files changed (309) hide show
  1. opscloud/__init__.py +5 -0
  2. opscloud/__main__.py +6 -0
  3. opscloud/_constants.py +53 -0
  4. opscloud/_debug.py +145 -0
  5. opscloud/_debug_buffer.py +250 -0
  6. opscloud/_textual_patches.py +367 -0
  7. opscloud/_version.py +15 -0
  8. opscloud/agent/__init__.py +27 -0
  9. opscloud/agent/config.py +168 -0
  10. opscloud/agent/factory.py +1103 -0
  11. opscloud/approval_mode.py +44 -0
  12. opscloud/backend/__init__.py +49 -0
  13. opscloud/backend/composite.py +89 -0
  14. opscloud/backend/local.py +183 -0
  15. opscloud/backend/registry.py +79 -0
  16. opscloud/backend/sandbox_config.py +107 -0
  17. opscloud/backend/sandbox_factory.py +432 -0
  18. opscloud/built_in_skills/remember/SKILL.md +124 -0
  19. opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
  20. opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
  21. opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
  22. opscloud/cli/__init__.py +1 -0
  23. opscloud/cli/commands/__init__.py +15 -0
  24. opscloud/cli/commands/auth.py +45 -0
  25. opscloud/cli/commands/doctor.py +100 -0
  26. opscloud/cli/commands/mcp.py +137 -0
  27. opscloud/cli/commands/threads.py +51 -0
  28. opscloud/cli/interactive.py +154 -0
  29. opscloud/cli/main.py +903 -0
  30. opscloud/cli/non_interactive.py +425 -0
  31. opscloud/cli/server_manager.py +58 -0
  32. opscloud/commands/__init__.py +19 -0
  33. opscloud/commands/_base.py +110 -0
  34. opscloud/commands/_guards.py +52 -0
  35. opscloud/commands/_router.py +146 -0
  36. opscloud/commands/_types.py +36 -0
  37. opscloud/commands/core/__init__.py +47 -0
  38. opscloud/commands/core/auth.py +94 -0
  39. opscloud/commands/core/bug.py +46 -0
  40. opscloud/commands/core/clear.py +106 -0
  41. opscloud/commands/core/cloud.py +185 -0
  42. opscloud/commands/core/compact.py +128 -0
  43. opscloud/commands/core/config_cmd.py +120 -0
  44. opscloud/commands/core/context.py +220 -0
  45. opscloud/commands/core/cost.py +44 -0
  46. opscloud/commands/core/doctor.py +535 -0
  47. opscloud/commands/core/effort.py +108 -0
  48. opscloud/commands/core/exit_cmd.py +41 -0
  49. opscloud/commands/core/fast.py +148 -0
  50. opscloud/commands/core/help_cmd.py +94 -0
  51. opscloud/commands/core/mcp.py +134 -0
  52. opscloud/commands/core/model.py +109 -0
  53. opscloud/commands/core/permissions.py +196 -0
  54. opscloud/commands/core/plugins.py +92 -0
  55. opscloud/commands/core/pool.py +108 -0
  56. opscloud/commands/core/resume.py +97 -0
  57. opscloud/commands/core/skills.py +178 -0
  58. opscloud/commands/power/__init__.py +51 -0
  59. opscloud/commands/power/agents.py +98 -0
  60. opscloud/commands/power/btw.py +85 -0
  61. opscloud/commands/power/copy.py +97 -0
  62. opscloud/commands/power/goal.py +548 -0
  63. opscloud/commands/power/loop.py +295 -0
  64. opscloud/commands/power/memory.py +287 -0
  65. opscloud/commands/power/review.py +152 -0
  66. opscloud/commands/power/rubric.py +287 -0
  67. opscloud/commands/power/runtime.py +321 -0
  68. opscloud/commands/power/skill_creator.py +70 -0
  69. opscloud/commands/power/skill_invoke.py +165 -0
  70. opscloud/commands/power/tasks.py +209 -0
  71. opscloud/commands/power/trace.py +117 -0
  72. opscloud/commands/power/ui_toggles.py +135 -0
  73. opscloud/commands/power/version.py +52 -0
  74. opscloud/commands/registry.py +176 -0
  75. opscloud/config/__init__.py +114 -0
  76. opscloud/config/adapters/__init__.py +5 -0
  77. opscloud/config/adapters/postgres.py +776 -0
  78. opscloud/config/adapters/sqlite.py +769 -0
  79. opscloud/config/aws.py +248 -0
  80. opscloud/config/cloud_profiles.py +437 -0
  81. opscloud/config/env_vars.py +81 -0
  82. opscloud/config/langsmith.py +319 -0
  83. opscloud/config/manifest.py +817 -0
  84. opscloud/config/metadata.py +205 -0
  85. opscloud/config/paths.py +604 -0
  86. opscloud/config/plugins.py +130 -0
  87. opscloud/config/settings.py +834 -0
  88. opscloud/config/store.py +453 -0
  89. opscloud/config/store_factory.py +97 -0
  90. opscloud/config/toml_config.py +562 -0
  91. opscloud/editor.py +173 -0
  92. opscloud/exceptions.py +65 -0
  93. opscloud/file_ops.py +217 -0
  94. opscloud/hooks/__init__.py +139 -0
  95. opscloud/hooks/env.py +44 -0
  96. opscloud/hooks/interrupt.py +121 -0
  97. opscloud/hooks/models/__init__.py +109 -0
  98. opscloud/hooks/models/adapters.py +19 -0
  99. opscloud/hooks/models/config.py +79 -0
  100. opscloud/hooks/models/domain.py +434 -0
  101. opscloud/hooks/models/transport.py +37 -0
  102. opscloud/hooks/tools.py +231 -0
  103. opscloud/input.py +48 -0
  104. opscloud/integrations/__init__.py +80 -0
  105. opscloud/integrations/base.py +153 -0
  106. opscloud/integrations/content_filter.py +119 -0
  107. opscloud/integrations/event_bus.py +480 -0
  108. opscloud/integrations/hooks.py +238 -0
  109. opscloud/integrations/identity.py +181 -0
  110. opscloud/integrations/notifications.py +134 -0
  111. opscloud/integrations/stream_bridge.py +397 -0
  112. opscloud/integrations/thread_store.py +81 -0
  113. opscloud/json_types.py +20 -0
  114. opscloud/mcp/__init__.py +57 -0
  115. opscloud/mcp/config.py +203 -0
  116. opscloud/mcp/discovery.py +229 -0
  117. opscloud/mcp/mcp_info.py +109 -0
  118. opscloud/mcp/middleware.py +102 -0
  119. opscloud/mcp/preload.py +505 -0
  120. opscloud/mcp/raw_config.py +149 -0
  121. opscloud/mcp/semantic_profiler.py +305 -0
  122. opscloud/mcp/session_manager.py +727 -0
  123. opscloud/mcp/trust.py +105 -0
  124. opscloud/memory/__init__.py +27 -0
  125. opscloud/memory/branch.py +61 -0
  126. opscloud/memory/guard.py +19 -0
  127. opscloud/memory/onboarding.py +120 -0
  128. opscloud/memory/registry.py +134 -0
  129. opscloud/memory/store.py +223 -0
  130. opscloud/middleware/__init__.py +106 -0
  131. opscloud/middleware/_repository_bounds.py +459 -0
  132. opscloud/middleware/ask_user.py +152 -0
  133. opscloud/middleware/auto_mode.py +60 -0
  134. opscloud/middleware/auto_mode_hitl.py +2434 -0
  135. opscloud/middleware/compaction.py +566 -0
  136. opscloud/middleware/configurable_model.py +233 -0
  137. opscloud/middleware/cost_tracking.py +1021 -0
  138. opscloud/middleware/glm_stall_recovery.py +109 -0
  139. opscloud/middleware/goal_criteria.py +1234 -0
  140. opscloud/middleware/goal_state_notice.py +712 -0
  141. opscloud/middleware/goal_tools.py +407 -0
  142. opscloud/middleware/headless_mcp_guard.py +88 -0
  143. opscloud/middleware/jev_model_router.py +1018 -0
  144. opscloud/middleware/local_context.py +460 -0
  145. opscloud/middleware/mcp_context.py +151 -0
  146. opscloud/middleware/mcp_middleware.py +146 -0
  147. opscloud/middleware/memory_guard.py +393 -0
  148. opscloud/middleware/model_retry.py +205 -0
  149. opscloud/middleware/registry.py +137 -0
  150. opscloud/middleware/reliable_rubric.py +709 -0
  151. opscloud/middleware/resume_state.py +91 -0
  152. opscloud/middleware/server_hooks.py +1189 -0
  153. opscloud/middleware/shell_allow_list.py +90 -0
  154. opscloud/middleware/skills.py +382 -0
  155. opscloud/middleware/subagent_artifacts.py +84 -0
  156. opscloud/middleware/subagent_telemetry.py +261 -0
  157. opscloud/middleware/subagents.py +243 -0
  158. opscloud/middleware/tool_filter.py +384 -0
  159. opscloud/middleware/unified_system_message.py +62 -0
  160. opscloud/model/__init__.py +37 -0
  161. opscloud/model/config.py +2992 -0
  162. opscloud/model/factory.py +477 -0
  163. opscloud/model/pool.py +459 -0
  164. opscloud/model/reasoning.py +282 -0
  165. opscloud/offload.py +173 -0
  166. opscloud/output.py +64 -0
  167. opscloud/plugins/__init__.py +189 -0
  168. opscloud/plugins/_json.py +22 -0
  169. opscloud/plugins/adapters/__init__.py +13 -0
  170. opscloud/plugins/adapters/agents.py +112 -0
  171. opscloud/plugins/adapters/commands.py +190 -0
  172. opscloud/plugins/adapters/mcp.py +363 -0
  173. opscloud/plugins/adapters/skills.py +132 -0
  174. opscloud/plugins/commands_cli.py +541 -0
  175. opscloud/plugins/discovery.py +512 -0
  176. opscloud/plugins/manifest.py +335 -0
  177. opscloud/plugins/marketplace.py +893 -0
  178. opscloud/plugins/models.py +334 -0
  179. opscloud/plugins/project_plugins.py +317 -0
  180. opscloud/plugins/protocol.py +31 -0
  181. opscloud/plugins/store.py +905 -0
  182. opscloud/plugins/substitution.py +122 -0
  183. opscloud/project_utils.py +145 -0
  184. opscloud/prompts/__init__.py +37 -0
  185. opscloud/prompts/system.py +483 -0
  186. opscloud/prompts/templates/system_prompt.md +166 -0
  187. opscloud/prompts/types.py +54 -0
  188. opscloud/rubrics/__init__.py +42 -0
  189. opscloud/rubrics/evaluator.py +404 -0
  190. opscloud/rubrics/evidence_extractor.py +229 -0
  191. opscloud/rubrics/generator.py +239 -0
  192. opscloud/rubrics/jev_compiler.py +175 -0
  193. opscloud/rubrics/jev_grader.py +297 -0
  194. opscloud/rubrics/middleware.py +8 -0
  195. opscloud/schema/__init__.py +107 -0
  196. opscloud/schema/interrupts.py +981 -0
  197. opscloud/security/__init__.py +91 -0
  198. opscloud/security/approval_mode.py +528 -0
  199. opscloud/security/approval_mode_source.py +230 -0
  200. opscloud/security/cli_ast_evaluator.py +552 -0
  201. opscloud/security/jev_classifier.py +651 -0
  202. opscloud/security/shell_safety.py +340 -0
  203. opscloud/security/unicode_security.py +439 -0
  204. opscloud/security/url_validation.py +160 -0
  205. opscloud/server/__init__.py +13 -0
  206. opscloud/server/_server_config.py +158 -0
  207. opscloud/server/server.py +242 -0
  208. opscloud/server/server_graph.py +93 -0
  209. opscloud/skills/__init__.py +91 -0
  210. opscloud/skills/commands.py +1099 -0
  211. opscloud/skills/invocation.py +200 -0
  212. opscloud/skills/load.py +388 -0
  213. opscloud/skills/loader.py +23 -0
  214. opscloud/skills/merge.py +65 -0
  215. opscloud/skills/registry.py +263 -0
  216. opscloud/skills/sources.py +174 -0
  217. opscloud/skills/trust.py +417 -0
  218. opscloud/state/__init__.py +142 -0
  219. opscloud/state/base.py +154 -0
  220. opscloud/state/cloud_context.py +262 -0
  221. opscloud/state/goal_channels.py +124 -0
  222. opscloud/state/goal_state_limits.py +175 -0
  223. opscloud/state/resume_state.py +58 -0
  224. opscloud/state/service_context.py +281 -0
  225. opscloud/state/session.py +957 -0
  226. opscloud/state/state_migration.py +86 -0
  227. opscloud/subagents/__init__.py +30 -0
  228. opscloud/subagents/loader.py +331 -0
  229. opscloud/subagents/subagents_parser.py +117 -0
  230. opscloud/subagents/types.py +25 -0
  231. opscloud/tools/__init__.py +36 -0
  232. opscloud/tools/catalog.py +37 -0
  233. opscloud/tools/display.py +24 -0
  234. opscloud/tools/fetch_url.py +176 -0
  235. opscloud/tools/goal_tools.py +423 -0
  236. opscloud/tools/registry.py +105 -0
  237. opscloud/tools/thread.py +32 -0
  238. opscloud/tools/web_search.py +215 -0
  239. opscloud/ui/__init__.py +119 -0
  240. opscloud/ui/app.py +5400 -0
  241. opscloud/ui/app.tcss +479 -0
  242. opscloud/ui/clipboard.py +147 -0
  243. opscloud/ui/command_registry.py +482 -0
  244. opscloud/ui/message_store.py +152 -0
  245. opscloud/ui/permission_store.py +360 -0
  246. opscloud/ui/preferences.py +113 -0
  247. opscloud/ui/prompt_manager.py +96 -0
  248. opscloud/ui/remote_client.py +505 -0
  249. opscloud/ui/textual_adapter.py +1269 -0
  250. opscloud/ui/theme.py +728 -0
  251. opscloud/ui/ui_help.py +415 -0
  252. opscloud/ui/widgets/__init__.py +383 -0
  253. opscloud/ui/widgets/_ask_user_types.py +81 -0
  254. opscloud/ui/widgets/_copy_spans.py +52 -0
  255. opscloud/ui/widgets/_inline_prompt.py +290 -0
  256. opscloud/ui/widgets/_js_eval_display.py +139 -0
  257. opscloud/ui/widgets/_links.py +175 -0
  258. opscloud/ui/widgets/_tool_stream.py +308 -0
  259. opscloud/ui/widgets/agent_selector.py +297 -0
  260. opscloud/ui/widgets/approval.py +741 -0
  261. opscloud/ui/widgets/ask_user.py +539 -0
  262. opscloud/ui/widgets/auth.py +695 -0
  263. opscloud/ui/widgets/auth_manager.py +214 -0
  264. opscloud/ui/widgets/auto_mode_notice.py +146 -0
  265. opscloud/ui/widgets/autocomplete.py +709 -0
  266. opscloud/ui/widgets/chat_input.py +805 -0
  267. opscloud/ui/widgets/cloud_selector.py +528 -0
  268. opscloud/ui/widgets/config_manager.py +652 -0
  269. opscloud/ui/widgets/debug_console.py +1269 -0
  270. opscloud/ui/widgets/devops_renderers.py +184 -0
  271. opscloud/ui/widgets/diff.py +237 -0
  272. opscloud/ui/widgets/effort_selector.py +166 -0
  273. opscloud/ui/widgets/goal_review.py +612 -0
  274. opscloud/ui/widgets/goal_status.py +57 -0
  275. opscloud/ui/widgets/infra_panel.py +141 -0
  276. opscloud/ui/widgets/install_confirm.py +114 -0
  277. opscloud/ui/widgets/loading.py +170 -0
  278. opscloud/ui/widgets/mcp_viewer.py +1204 -0
  279. opscloud/ui/widgets/messages.py +4461 -0
  280. opscloud/ui/widgets/model_selector.py +863 -0
  281. opscloud/ui/widgets/notification_center.py +108 -0
  282. opscloud/ui/widgets/notification_settings.py +119 -0
  283. opscloud/ui/widgets/operation_card.py +106 -0
  284. opscloud/ui/widgets/permissions_manager.py +667 -0
  285. opscloud/ui/widgets/plugin_manager.py +1212 -0
  286. opscloud/ui/widgets/pool_selector.py +376 -0
  287. opscloud/ui/widgets/preamble.py +53 -0
  288. opscloud/ui/widgets/skills_viewer.py +432 -0
  289. opscloud/ui/widgets/status.py +1037 -0
  290. opscloud/ui/widgets/subagent_panel.py +927 -0
  291. opscloud/ui/widgets/theme_selector.py +232 -0
  292. opscloud/ui/widgets/thread_selector.py +358 -0
  293. opscloud/ui/widgets/toast.py +31 -0
  294. opscloud/ui/widgets/tool_display.py +325 -0
  295. opscloud/ui/widgets/tool_renderers.py +234 -0
  296. opscloud/ui/widgets/tool_widgets.py +278 -0
  297. opscloud/ui/widgets/welcome.py +421 -0
  298. opscloud/ui/widgets/welcome_popup.py +102 -0
  299. opscloud/utils/__init__.py +24 -0
  300. opscloud/utils/cost_estimation.py +169 -0
  301. opscloud/utils/git.py +390 -0
  302. opscloud/utils/logger.py +571 -0
  303. opscloud/utils/session_stats.py +115 -0
  304. opscloud/utils/startup_error.py +37 -0
  305. talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
  306. talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
  307. talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
  308. talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
  309. 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
+ ]