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
opscloud/config/store.py
ADDED
|
@@ -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)
|