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,453 @@
1
+ """DB-backed configuration store with manifest-aware typed resolution.
2
+
3
+ The ``ConfigStore`` facade provides:
4
+ - **Typed resolution**: ``resolve(option)`` → DB → env → manifest default
5
+ - **Entity management**: MCP servers, plugins, skills, subagents, model preferences
6
+ - **In-memory cache**: Warm cache avoids DB round-trips for hot paths
7
+ - **Adapter pattern**: ``ConfigStorageAdapter`` ABC for SQLite/Postgres backends
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from abc import ABC, abstractmethod
13
+ from dataclasses import dataclass
14
+ from enum import StrEnum
15
+ from typing import Any
16
+
17
+ from opscloud.utils.logger import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class ConfigCategory(StrEnum):
23
+ """Categories for grouping configuration entries."""
24
+
25
+ CREDENTIALS = "credentials"
26
+ LLM = "llm"
27
+ MODELS = "models"
28
+ AWS = "aws"
29
+ SECURITY = "security"
30
+ SANDBOX = "sandbox"
31
+ MCP = "mcp"
32
+ MCP_SERVERS = "mcp_servers"
33
+ COMPACTION = "compaction"
34
+ RUBRIC = "rubric"
35
+ INTEGRATIONS = "integrations"
36
+ A2A = "a2a"
37
+ SYSTEM = "system"
38
+ PROJECT = "project"
39
+ STORAGE = "storage"
40
+ TRACING = "tracing"
41
+ SEARCH = "search"
42
+ MONITORING = "monitoring"
43
+
44
+
45
+ _GROUP_TO_CATEGORY: dict[str, ConfigCategory] = {
46
+ "Credentials": ConfigCategory.CREDENTIALS,
47
+ "Models": ConfigCategory.MODELS,
48
+ "AWS": ConfigCategory.AWS,
49
+ "Security": ConfigCategory.SECURITY,
50
+ "Sandbox": ConfigCategory.SANDBOX,
51
+ "Storage": ConfigCategory.STORAGE,
52
+ "MCP": ConfigCategory.MCP,
53
+ "MCP Servers": ConfigCategory.MCP_SERVERS,
54
+ "Compaction": ConfigCategory.COMPACTION,
55
+ "Rubric": ConfigCategory.RUBRIC,
56
+ "Integrations": ConfigCategory.INTEGRATIONS,
57
+ "A2A": ConfigCategory.A2A,
58
+ "System": ConfigCategory.SYSTEM,
59
+ "Project": ConfigCategory.PROJECT,
60
+ "Tracing": ConfigCategory.TRACING,
61
+ "Search": ConfigCategory.SEARCH,
62
+ }
63
+
64
+
65
+ def category_from_group(group: str) -> ConfigCategory:
66
+ """Map a manifest group name to a ConfigCategory."""
67
+ return _GROUP_TO_CATEGORY.get(group, ConfigCategory.SYSTEM)
68
+
69
+
70
+ @dataclass
71
+ class ConfigEntry:
72
+ """A single configuration entry stored in the DB."""
73
+
74
+ key: str
75
+ value: str
76
+ category: ConfigCategory = ConfigCategory.SYSTEM
77
+ display_name: str = ""
78
+ description: str = ""
79
+ is_secret: bool = False
80
+
81
+ def __post_init__(self) -> None:
82
+ if not self.display_name:
83
+ self.display_name = self.key.replace("_", " ").title()
84
+
85
+
86
+ ConfigValue = ConfigEntry
87
+
88
+
89
+ class ConfigStorageAdapter(ABC):
90
+ """Abstract base class for config storage backends."""
91
+
92
+ @abstractmethod
93
+ async def initialize(self) -> None:
94
+ """Create tables/schema if needed."""
95
+
96
+ # ── Config Entry CRUD ─────────────────────────────────
97
+ @abstractmethod
98
+ async def load_all(self) -> list[ConfigEntry]:
99
+ """Load all configuration entries."""
100
+
101
+ @abstractmethod
102
+ async def get(self, key: str) -> ConfigEntry | None:
103
+ """Get a single config entry by key."""
104
+
105
+ @abstractmethod
106
+ async def set(self, entry: ConfigEntry) -> None:
107
+ """Set/upsert a configuration entry."""
108
+
109
+ @abstractmethod
110
+ async def delete(self, key: str) -> bool:
111
+ """Delete a config entry."""
112
+
113
+ @abstractmethod
114
+ async def list_by_category(self, category: ConfigCategory) -> list[ConfigEntry]:
115
+ """List all entries in a category."""
116
+
117
+ # ── Model Preferences CRUD ────────────────────────────
118
+ @abstractmethod
119
+ async def get_model_preferences(self) -> dict[str, Any]:
120
+ """Get default model, recent models, effort-by-model, and provider configs."""
121
+
122
+ @abstractmethod
123
+ async def save_model_preferences(self, prefs: dict[str, Any]) -> None:
124
+ """Save model preferences."""
125
+
126
+ # ── MCP Server CRUD ──────────────────────────────────
127
+ @abstractmethod
128
+ async def list_mcp_servers(self) -> list[dict[str, Any]]:
129
+ """List all MCP server configurations."""
130
+
131
+ @abstractmethod
132
+ async def get_mcp_server(self, name: str) -> dict[str, Any] | None:
133
+ """Get a single MCP server configuration."""
134
+
135
+ @abstractmethod
136
+ async def upsert_mcp_server(self, server: dict[str, Any]) -> None:
137
+ """Upsert an MCP server configuration."""
138
+
139
+ @abstractmethod
140
+ async def delete_mcp_server(self, name: str) -> bool:
141
+ """Delete an MCP server by name."""
142
+
143
+ # ── Marketplace Metadata CRUD ─────────────────────────
144
+ @abstractmethod
145
+ async def list_marketplaces(self) -> list[dict[str, Any]]:
146
+ """List all marketplace records."""
147
+
148
+ @abstractmethod
149
+ async def get_marketplace(self, name: str) -> dict[str, Any] | None:
150
+ """Get a single marketplace record by name."""
151
+
152
+ @abstractmethod
153
+ async def upsert_marketplace(self, marketplace: dict[str, Any]) -> None:
154
+ """Upsert a marketplace record."""
155
+
156
+ @abstractmethod
157
+ async def delete_marketplace(self, name: str) -> bool:
158
+ """Delete a marketplace record."""
159
+
160
+ # ── Plugin Metadata CRUD ──────────────────────────────
161
+ @abstractmethod
162
+ async def list_plugins(self) -> list[dict[str, Any]]:
163
+ """List all installed plugin records."""
164
+
165
+ @abstractmethod
166
+ async def get_plugin(self, plugin_id: str) -> dict[str, Any] | None:
167
+ """Get a single plugin record by plugin_id."""
168
+
169
+ @abstractmethod
170
+ async def upsert_plugin(self, plugin: dict[str, Any]) -> None:
171
+ """Upsert a plugin record."""
172
+
173
+ @abstractmethod
174
+ async def delete_plugin(self, plugin_id: str) -> bool:
175
+ """Delete a plugin record."""
176
+
177
+ # ── Skill Metadata CRUD ───────────────────────────────
178
+ @abstractmethod
179
+ async def list_skills(self) -> list[dict[str, Any]]:
180
+ """List all skill metadata records."""
181
+
182
+ @abstractmethod
183
+ async def upsert_skill(self, skill: dict[str, Any]) -> None:
184
+ """Upsert a skill metadata record."""
185
+
186
+ @abstractmethod
187
+ async def delete_skill(self, name: str) -> bool:
188
+ """Delete a skill record."""
189
+
190
+ # ── Subagent Metadata CRUD ────────────────────────────
191
+ @abstractmethod
192
+ async def list_subagents(self) -> list[dict[str, Any]]:
193
+ """List all subagent metadata records."""
194
+
195
+ @abstractmethod
196
+ async def upsert_subagent(self, subagent: dict[str, Any]) -> None:
197
+ """Upsert a subagent metadata record."""
198
+
199
+ @abstractmethod
200
+ async def delete_subagent(self, name: str) -> bool:
201
+ """Delete a subagent record."""
202
+
203
+ # ── Agent Instruction Metadata ────────────────────────
204
+ @abstractmethod
205
+ async def list_agent_instructions(self) -> list[dict[str, Any]]:
206
+ """List all agent instruction metadata."""
207
+
208
+ @abstractmethod
209
+ async def upsert_agent_instruction(self, instruction: dict[str, Any]) -> None:
210
+ """Upsert an agent instruction metadata entry."""
211
+
212
+
213
+ class ConfigStore:
214
+ """Facade for configuration with manifest-aware typed resolution & entity management."""
215
+
216
+ def __init__(self, adapter: ConfigStorageAdapter) -> None:
217
+ """Initialize the ConfigStore with a storage adapter."""
218
+ self._adapter = adapter
219
+ self._cache: dict[str, ConfigEntry] = {}
220
+ self._initialized = False
221
+
222
+ @property
223
+ def adapter(self) -> ConfigStorageAdapter:
224
+ """Return the underlying configuration storage adapter."""
225
+ return self._adapter
226
+
227
+ async def initialize(self) -> None:
228
+ """Initialize storage backend and warm cache from DB."""
229
+ if self._initialized:
230
+ return
231
+ await self._adapter.initialize()
232
+ entries = await self._adapter.load_all()
233
+ for entry in entries:
234
+ self._cache[entry.key] = entry
235
+ self._initialized = True
236
+ logger.info("ConfigStore initialized with %d entries", len(self._cache))
237
+
238
+ # ── Typed Resolution ─────────────────────────────────
239
+
240
+ async def resolve(
241
+ self,
242
+ option: Any, # ConfigOption
243
+ ) -> tuple[Any, str]:
244
+ """Resolve an option through the chain: DB → env → TOML → manifest default."""
245
+ from opscloud.config.manifest import (
246
+ _coerce_toml,
247
+ _toml_lookup,
248
+ coerce_str_value,
249
+ load_config_toml,
250
+ resolve_from_env,
251
+ )
252
+
253
+ # 1. Check DB
254
+ db_key = getattr(option, "db_key", None) or getattr(option, "key", None)
255
+ if db_key:
256
+ db_value = await self.get(db_key)
257
+ if db_value is not None and db_value != "":
258
+ coerced = coerce_str_value(option.kind, db_value)
259
+ if coerced is not None:
260
+ return coerced, "db"
261
+
262
+ # 2. Check environment variables
263
+ env_result = resolve_from_env(option)
264
+ if env_result is not None:
265
+ return env_result
266
+
267
+ # 3. Check TOML config file
268
+ toml_keys = getattr(option, "toml_keys", None)
269
+ if toml_keys:
270
+ toml_data = load_config_toml()
271
+ found, raw = _toml_lookup(toml_data, toml_keys)
272
+ if found:
273
+ coerced = _coerce_toml(option, raw)
274
+ if coerced is not None:
275
+ return coerced, "config.toml"
276
+
277
+ # 4. Manifest default
278
+ return option.default, "default"
279
+
280
+ async def get_typed(
281
+ self,
282
+ option: Any, # ConfigOption
283
+ ) -> Any:
284
+ """Resolve and return just the typed value."""
285
+ value, _ = await self.resolve(option)
286
+ return value
287
+
288
+ async def set_typed(
289
+ self,
290
+ option: Any, # ConfigOption
291
+ value: Any,
292
+ ) -> None:
293
+ """Set a config value with manifest-aware serialization."""
294
+ from opscloud.config.manifest import serialize_typed_value
295
+
296
+ str_value = serialize_typed_value(option.kind, value)
297
+ await self.set(
298
+ key=option.db_key,
299
+ value=str_value,
300
+ category=category_from_group(option.group),
301
+ display_name=option.summary,
302
+ description="",
303
+ is_secret=option.redacted,
304
+ )
305
+
306
+ # ── Raw String Operations ────────────────────────────
307
+
308
+ async def get_entry(self, key: str) -> ConfigEntry | None:
309
+ """Get a full config entry by key (cache → adapter)."""
310
+ if key in self._cache:
311
+ return self._cache[key]
312
+
313
+ entry = await self._adapter.get(key)
314
+ if entry:
315
+ self._cache[key] = entry
316
+ return entry
317
+
318
+ return None
319
+
320
+ async def get(self, key: str, default: str | None = None) -> str | None:
321
+ """Get a raw config value by key (cache → adapter → default)."""
322
+ entry = await self.get_entry(key)
323
+ if entry is not None:
324
+ return entry.value
325
+ return default
326
+
327
+ async def set(
328
+ self,
329
+ key: str,
330
+ value: str,
331
+ *,
332
+ category: ConfigCategory = ConfigCategory.SYSTEM,
333
+ display_name: str = "",
334
+ description: str = "",
335
+ is_secret: bool = False,
336
+ ) -> None:
337
+ """Set/upsert a config value in the DB and cache."""
338
+ entry = ConfigEntry(
339
+ key=key,
340
+ value=value,
341
+ category=category,
342
+ display_name=display_name,
343
+ description=description,
344
+ is_secret=is_secret,
345
+ )
346
+ await self._adapter.set(entry)
347
+ self._cache[key] = entry
348
+
349
+ async def delete(self, key: str) -> bool:
350
+ """Delete a config entry from DB and cache."""
351
+ result = await self._adapter.delete(key)
352
+ self._cache.pop(key, None)
353
+ return result
354
+
355
+ async def list_by_category(self, category: ConfigCategory) -> list[ConfigEntry]:
356
+ """List all entries in a category."""
357
+ return await self._adapter.list_by_category(category)
358
+
359
+ async def list_all(self) -> list[ConfigEntry]:
360
+ """List all entries from the adapter."""
361
+ return await self._adapter.load_all()
362
+
363
+ async def invalidate_cache(self, key: str | None = None) -> None:
364
+ """Invalidate cache for a specific key or all keys."""
365
+ if key:
366
+ self._cache.pop(key, None)
367
+ else:
368
+ self._cache.clear()
369
+ entries = await self._adapter.load_all()
370
+ for entry in entries:
371
+ self._cache[entry.key] = entry
372
+
373
+ # ── Entity Convenience Passthroughs ──────────────────
374
+
375
+ async def get_model_preferences(self) -> dict[str, Any]:
376
+ """Retrieve saved model preferences."""
377
+ return await self._adapter.get_model_preferences()
378
+
379
+ async def save_model_preferences(self, prefs: dict[str, Any]) -> None:
380
+ """Save or update model preferences."""
381
+ await self._adapter.save_model_preferences(prefs)
382
+
383
+ async def list_mcp_servers(self) -> list[dict[str, Any]]:
384
+ """List all configured MCP servers."""
385
+ return await self._adapter.list_mcp_servers()
386
+
387
+ async def get_mcp_server(self, name: str) -> dict[str, Any] | None:
388
+ """Retrieve an MCP server configuration by name."""
389
+ return await self._adapter.get_mcp_server(name)
390
+
391
+ async def upsert_mcp_server(self, server: dict[str, Any]) -> None:
392
+ """Create or update an MCP server configuration."""
393
+ await self._adapter.upsert_mcp_server(server)
394
+
395
+ async def delete_mcp_server(self, name: str) -> bool:
396
+ """Delete an MCP server configuration by name."""
397
+ return await self._adapter.delete_mcp_server(name)
398
+
399
+ async def list_marketplaces(self) -> list[dict[str, Any]]:
400
+ """List all configured plugin marketplaces."""
401
+ return await self._adapter.list_marketplaces()
402
+
403
+ async def get_marketplace(self, name: str) -> dict[str, Any] | None:
404
+ """Retrieve a plugin marketplace by name."""
405
+ return await self._adapter.get_marketplace(name)
406
+
407
+ async def upsert_marketplace(self, marketplace: dict[str, Any]) -> None:
408
+ """Create or update a plugin marketplace record."""
409
+ await self._adapter.upsert_marketplace(marketplace)
410
+
411
+ async def delete_marketplace(self, name: str) -> bool:
412
+ """Delete a plugin marketplace by name."""
413
+ return await self._adapter.delete_marketplace(name)
414
+
415
+ async def list_plugins(self) -> list[dict[str, Any]]:
416
+ """List all installed plugin records."""
417
+ return await self._adapter.list_plugins()
418
+
419
+ async def get_plugin(self, plugin_id: str) -> dict[str, Any] | None:
420
+ """Retrieve an installed plugin record by ID."""
421
+ return await self._adapter.get_plugin(plugin_id)
422
+
423
+ async def upsert_plugin(self, plugin: dict[str, Any]) -> None:
424
+ """Create or update an installed plugin record."""
425
+ await self._adapter.upsert_plugin(plugin)
426
+
427
+ async def delete_plugin(self, plugin_id: str) -> bool:
428
+ """Delete an installed plugin record by ID."""
429
+ return await self._adapter.delete_plugin(plugin_id)
430
+
431
+ async def list_skills(self) -> list[dict[str, Any]]:
432
+ """List all stored custom skill records."""
433
+ return await self._adapter.list_skills()
434
+
435
+ async def upsert_skill(self, skill: dict[str, Any]) -> None:
436
+ """Create or update a custom skill record."""
437
+ await self._adapter.upsert_skill(skill)
438
+
439
+ async def delete_skill(self, name: str) -> bool:
440
+ """Delete a custom skill record by name."""
441
+ return await self._adapter.delete_skill(name)
442
+
443
+ async def list_subagents(self) -> list[dict[str, Any]]:
444
+ """List all stored custom subagent records."""
445
+ return await self._adapter.list_subagents()
446
+
447
+ async def upsert_subagent(self, subagent: dict[str, Any]) -> None:
448
+ """Create or update a custom subagent record."""
449
+ await self._adapter.upsert_subagent(subagent)
450
+
451
+ async def delete_subagent(self, name: str) -> bool:
452
+ """Delete a custom subagent record by name."""
453
+ return await self._adapter.delete_subagent(name)
@@ -0,0 +1,97 @@
1
+ """Config store factory — auto-detects postgres vs sqlite backend.
2
+
3
+ Uses the same connection-detection pattern as the checkpointer
4
+ (``state/checkpointer.py``): if ``OPSCLOUD_POSTGRES_URI`` or
5
+ ``DATABASE_URL`` is set, use PostgreSQL; otherwise fall back to SQLite.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+
12
+ from opscloud.config.store import ConfigStore
13
+ from opscloud.utils.logger import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ def _has_postgres_config() -> bool:
19
+ """Check if PostgreSQL connection is configured."""
20
+ return bool(
21
+ os.environ.get("POSTGRES_URI") or os.environ.get("OPSCLOUD_POSTGRES_URI") or os.environ.get("DATABASE_URL")
22
+ )
23
+
24
+
25
+ async def create_config_store(backend: str = "auto") -> ConfigStore:
26
+ """Create and initialize a ConfigStore.
27
+
28
+ Args:
29
+ backend: ``"auto"`` (detect from env), ``"postgres"``, or ``"sqlite"``.
30
+
31
+ Returns:
32
+ Initialized ``ConfigStore`` instance ready for reads/writes.
33
+ """
34
+ if backend == "auto":
35
+ backend = "postgres" if _has_postgres_config() else "sqlite"
36
+
37
+ if backend == "postgres":
38
+ from opscloud.config.adapters.postgres import PostgresConfigAdapter
39
+
40
+ uri = (
41
+ os.environ.get("POSTGRES_URI")
42
+ or os.environ.get("OPSCLOUD_POSTGRES_URI")
43
+ or os.environ.get("DATABASE_URL", "")
44
+ )
45
+ adapter = PostgresConfigAdapter(uri)
46
+
47
+ elif backend == "sqlite":
48
+ from opscloud.config.adapters.sqlite import SqliteConfigAdapter
49
+ from opscloud.config.paths import STATE_DIR
50
+
51
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
52
+ db_path = STATE_DIR / "config.db"
53
+ adapter = SqliteConfigAdapter(db_path)
54
+
55
+ else:
56
+ raise ValueError(f"Unknown config store backend: {backend!r}")
57
+
58
+ store = ConfigStore(adapter)
59
+ await store.initialize()
60
+ logger.info("ConfigStore created with %s backend", backend)
61
+ return store
62
+
63
+
64
+ def create_config_store_sync(backend: str = "auto") -> ConfigStore:
65
+ """Create a ConfigStore synchronously with auto-detected backend.
66
+
67
+ Args:
68
+ backend: ``"auto"`` (detect from env), ``"postgres"``, or ``"sqlite"``.
69
+
70
+ Returns:
71
+ ``ConfigStore`` instance with auto-detected adapter.
72
+ """
73
+ if backend == "auto":
74
+ backend = "postgres" if _has_postgres_config() else "sqlite"
75
+
76
+ if backend == "postgres":
77
+ from opscloud.config.adapters.postgres import PostgresConfigAdapter
78
+
79
+ uri = (
80
+ os.environ.get("POSTGRES_URI")
81
+ or os.environ.get("OPSCLOUD_POSTGRES_URI")
82
+ or os.environ.get("DATABASE_URL", "")
83
+ )
84
+ adapter = PostgresConfigAdapter(uri)
85
+
86
+ elif backend == "sqlite":
87
+ from opscloud.config.adapters.sqlite import SqliteConfigAdapter
88
+ from opscloud.config.paths import STATE_DIR
89
+
90
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
91
+ db_path = STATE_DIR / "config.db"
92
+ adapter = SqliteConfigAdapter(db_path)
93
+
94
+ else:
95
+ raise ValueError(f"Unknown config store backend: {backend!r}")
96
+
97
+ return ConfigStore(adapter)