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,44 @@
1
+ """Approval modes for OpsCloud.
2
+
3
+ Re-exports canonical definitions and store/acknowledgement utilities from `opscloud.security.approval_mode`.
4
+ """
5
+
6
+ from opscloud.security.approval_mode import (
7
+ APPROVAL_MODE_NAMESPACE,
8
+ AUTO_NOTICE_VERSION,
9
+ ApprovalMode,
10
+ ApprovalModePayload,
11
+ approval_mode_key,
12
+ approval_mode_payload,
13
+ approval_mode_scope,
14
+ approval_state_path,
15
+ aread_approval_mode_from_store,
16
+ awrite_approval_mode,
17
+ coerce_approval_mode,
18
+ get_approval_mode,
19
+ has_auto_mode_notice,
20
+ next_approval_mode,
21
+ read_approval_mode_from_store,
22
+ save_auto_mode_notice,
23
+ set_approval_mode,
24
+ )
25
+
26
+ __all__ = [
27
+ "APPROVAL_MODE_NAMESPACE",
28
+ "AUTO_NOTICE_VERSION",
29
+ "ApprovalMode",
30
+ "ApprovalModePayload",
31
+ "approval_mode_key",
32
+ "approval_mode_payload",
33
+ "approval_mode_scope",
34
+ "approval_state_path",
35
+ "aread_approval_mode_from_store",
36
+ "awrite_approval_mode",
37
+ "coerce_approval_mode",
38
+ "get_approval_mode",
39
+ "has_auto_mode_notice",
40
+ "next_approval_mode",
41
+ "read_approval_mode_from_store",
42
+ "save_auto_mode_notice",
43
+ "set_approval_mode",
44
+ ]
@@ -0,0 +1,49 @@
1
+ """Backend module for OpsCloud — shell execution, sandbox environments, and filesystem access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from opscloud.backend.registry import (
8
+ BackendRegistry,
9
+ get_backend_class,
10
+ get_backend_registry,
11
+ register_backend,
12
+ )
13
+
14
+ __all__ = [
15
+ "BackendRegistry",
16
+ "CompositeBackend",
17
+ "LocalShellBackend",
18
+ "OpsCloudCompositeBackend",
19
+ "SandboxConfig",
20
+ "cleanup_all_sandboxes",
21
+ "cleanup_sandbox",
22
+ "clear_sandbox_cache",
23
+ "create_sandbox_backend",
24
+ "get_backend_class",
25
+ "get_backend_registry",
26
+ "get_sandbox_backend",
27
+ "register_backend",
28
+ ]
29
+
30
+
31
+ def __getattr__(name: str) -> Any:
32
+ """Lazy-load backend classes to avoid circular imports and unnecessary dependencies."""
33
+ _lazy_map = {
34
+ "LocalShellBackend": "opscloud.backend.local",
35
+ "OpsCloudCompositeBackend": "opscloud.backend.composite",
36
+ "CompositeBackend": "opscloud.backend.composite",
37
+ "SandboxConfig": "opscloud.backend.sandbox_config",
38
+ "get_sandbox_backend": "opscloud.backend.sandbox_factory",
39
+ "create_sandbox_backend": "opscloud.backend.sandbox_factory",
40
+ "cleanup_sandbox": "opscloud.backend.sandbox_factory",
41
+ "cleanup_all_sandboxes": "opscloud.backend.sandbox_factory",
42
+ "clear_sandbox_cache": "opscloud.backend.sandbox_factory",
43
+ }
44
+ if name in _lazy_map:
45
+ import importlib
46
+
47
+ mod = importlib.import_module(_lazy_map[name])
48
+ return getattr(mod, name)
49
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,89 @@
1
+ """Composite backend routing for OpsCloud.
2
+
3
+ Routes special virtual paths (``/large_tool_results/`` and ``/conversation_history/``)
4
+ to specialized backends while delegating all standard filesystem and command
5
+ execution to the default backend (e.g. ``LocalShellBackend`` or remote sandbox).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import atexit
11
+ from pathlib import Path
12
+ import shutil
13
+ import tempfile
14
+ from typing import Any
15
+
16
+ from deepagents.backends import (
17
+ BackendProtocol,
18
+ CompositeBackend as SDKCompositeBackend,
19
+ FilesystemBackend,
20
+ )
21
+
22
+ from opscloud.config.paths import ensure_conversation_history_dir
23
+
24
+
25
+ class OpsCloudCompositeBackend(SDKCompositeBackend):
26
+ """Composite backend routing virtual paths (/large_tool_results/, /conversation_history/)."""
27
+
28
+ def __init__(
29
+ self,
30
+ default: Any,
31
+ routes: dict[str, Any] | None = None,
32
+ **kwargs: Any,
33
+ ) -> None:
34
+ """Initialize OpsCloudCompositeBackend with default and routed backends.
35
+
36
+ Args:
37
+ default: Default backend instance to fall back to.
38
+ routes: Optional mapping of virtual path prefixes to backend instances.
39
+ **kwargs: Additional keyword arguments for SDKCompositeBackend.
40
+ """
41
+ self._large_results_dir = tempfile.mkdtemp(prefix="opscloud_large_results_")
42
+ self._conversation_history_dir = ensure_conversation_history_dir()
43
+
44
+ atexit.register(self.cleanup)
45
+
46
+ large_results_backend = FilesystemBackend(
47
+ root_dir=self._large_results_dir,
48
+ virtual_mode=True,
49
+ )
50
+ conversation_history_backend = FilesystemBackend(
51
+ root_dir=self._conversation_history_dir,
52
+ virtual_mode=True,
53
+ )
54
+
55
+ effective_routes: dict[str, BackendProtocol] = {
56
+ "/large_tool_results/": large_results_backend,
57
+ "/conversation_history/": conversation_history_backend,
58
+ }
59
+ if routes:
60
+ effective_routes.update(routes)
61
+
62
+ super().__init__(
63
+ default=default,
64
+ routes=effective_routes,
65
+ **kwargs,
66
+ )
67
+
68
+ def cleanup(self) -> None:
69
+ """Clean up temporary large tool result directories on shutdown.
70
+
71
+ Note: Conversation history is persistent and is intentionally preserved.
72
+ """
73
+ if hasattr(self, "_large_results_dir") and self._large_results_dir:
74
+ shutil.rmtree(self._large_results_dir, ignore_errors=True)
75
+
76
+ def __enter__(self) -> OpsCloudCompositeBackend:
77
+ return self
78
+
79
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
80
+ self.cleanup()
81
+
82
+
83
+ # Alias for convenience
84
+ CompositeBackend = OpsCloudCompositeBackend
85
+
86
+ __all__ = [
87
+ "CompositeBackend",
88
+ "OpsCloudCompositeBackend",
89
+ ]
@@ -0,0 +1,183 @@
1
+ """Local shell and filesystem execution backend for AWS operations.
2
+
3
+ Extends ``LocalShellBackend`` with a curated shell environment that preserves
4
+ AWS CLI, AWS CDK, Terraform/OpenTofu, Terragrunt, Kubernetes, and DevOps tool
5
+ credentials and environment configurations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from deepagents.backends import LocalShellBackend as SDKLocalShellBackend
15
+
16
+ from opscloud.backend.registry import register_backend
17
+ from opscloud.config.paths import AWS_PRESERVE_ENV_VARS
18
+
19
+ # Tool directories to prepend to PATH for CLI tool discovery
20
+ COMMON_TOOL_DIRS: tuple[str, ...] = (
21
+ "/opt/homebrew/bin",
22
+ "/usr/local/bin",
23
+ "~/.local/bin",
24
+ "~/.cargo/bin",
25
+ "~/.aws/bin",
26
+ )
27
+
28
+ # Prefix matching for AWS and Cloud IaC ecosystem tools
29
+ ALLOWED_ENV_PREFIXES: tuple[str, ...] = (
30
+ "AWS_",
31
+ "TF_VAR_",
32
+ "TF_",
33
+ "TERRAGRUNT_",
34
+ "KUBE_",
35
+ "HELM_",
36
+ "CDK_",
37
+ "PULUMI_",
38
+ "SAM_",
39
+ )
40
+
41
+
42
+ def _build_shell_env() -> dict[str, str]:
43
+ """Curate environment preserving safe shell vars, tool PATHs, and AWS/cloud credentials."""
44
+ safe_keys = {
45
+ "PATH",
46
+ "HOME",
47
+ "SHELL",
48
+ "TERM",
49
+ "LANG",
50
+ "USER",
51
+ "LOGNAME",
52
+ "PWD",
53
+ "EDITOR",
54
+ "VISUAL",
55
+ "LC_ALL",
56
+ "LOCALE",
57
+ }
58
+ env: dict[str, str] = {}
59
+ for key in safe_keys:
60
+ if key in os.environ:
61
+ env[key] = os.environ[key]
62
+
63
+ # Prepend standard binary locations to PATH
64
+ path_val = env.get("PATH", "")
65
+ for tool_dir in COMMON_TOOL_DIRS:
66
+ expanded = os.path.expanduser(tool_dir)
67
+ if expanded not in path_val.split(os.pathsep):
68
+ path_val = f"{expanded}{os.pathsep}{path_val}" if path_val else expanded
69
+ env["PATH"] = path_val
70
+
71
+ # Ensure AWS_PAGER is disabled by default so aws CLI commands do not hang waiting for paging
72
+ if "AWS_PAGER" in os.environ:
73
+ env["AWS_PAGER"] = os.environ["AWS_PAGER"]
74
+ else:
75
+ env["AWS_PAGER"] = ""
76
+
77
+ # Prevent interactive terminal prompts in subshells (e.g. Git credentials, CLI pagers)
78
+ env["GIT_TERMINAL_PROMPT"] = os.environ.get("GIT_TERMINAL_PROMPT", "0")
79
+ if "PAGER" not in env:
80
+ env["PAGER"] = os.environ.get("PAGER", "cat")
81
+
82
+ # Preserve all AWS & Cloud IaC environment variables
83
+ for key, val in os.environ.items():
84
+ if key in AWS_PRESERVE_ENV_VARS or key.startswith(ALLOWED_ENV_PREFIXES):
85
+ env[key] = val
86
+
87
+ return env
88
+
89
+
90
+ @register_backend("local")
91
+ class LocalShellBackend(SDKLocalShellBackend):
92
+ """Local shell and filesystem backend specialized for AWS DevOps/SRE operations."""
93
+
94
+ def __init__(
95
+ self,
96
+ root_dir: Path | str,
97
+ virtual_mode: bool = False,
98
+ inherit_env: bool = False,
99
+ env: dict[str, str] | None = None,
100
+ **kwargs: Any,
101
+ ) -> None:
102
+ """Initialize LocalShellBackend with sanitized AWS cloud environment.
103
+
104
+ Args:
105
+ root_dir: Root directory for file and command operations.
106
+ virtual_mode: Whether to restrict operations to virtual filesystem.
107
+ inherit_env: Whether to inherit parent process environment variables.
108
+ env: Explicit environment variables dictionary.
109
+ **kwargs: Additional keyword arguments for SDKLocalShellBackend.
110
+ """
111
+ effective_env = env if env is not None else _build_shell_env()
112
+ super().__init__(
113
+ root_dir=Path(root_dir),
114
+ virtual_mode=virtual_mode,
115
+ inherit_env=inherit_env,
116
+ env=effective_env,
117
+ **kwargs,
118
+ )
119
+
120
+ def _sync_cloud_env(self) -> None:
121
+ """Dynamically sync active AWS profile and credentials into the shell execution environment.
122
+
123
+ Reads latest persisted cloud profile from config.toml, ambient AWS_PROFILE,
124
+ and synchronizes AWS_PROFILE, AWS_DEFAULT_PROFILE, and region.
125
+ When an explicit named profile is active, purges any static conflicting
126
+ AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY from self._env so AWS CLI and boto3
127
+ properly use the profile's authentic credentials rather than failing.
128
+ """
129
+ from opscloud.config.cloud_profiles import get_active_aws_profile, get_active_aws_region
130
+
131
+ active_profile = get_active_aws_profile()
132
+ if active_profile:
133
+ self._env["AWS_PROFILE"] = active_profile
134
+ self._env["AWS_DEFAULT_PROFILE"] = active_profile
135
+ os.environ["AWS_PROFILE"] = active_profile
136
+ os.environ["AWS_DEFAULT_PROFILE"] = active_profile
137
+
138
+ # Clear static credentials when a named profile is selected so AWS CLI
139
+ # authentication precedence does not prioritize stale or unauthenticated env keys.
140
+ if active_profile != "default":
141
+ self._env.pop("AWS_ACCESS_KEY_ID", None)
142
+ self._env.pop("AWS_SECRET_ACCESS_KEY", None)
143
+ self._env.pop("AWS_SESSION_TOKEN", None)
144
+
145
+ active_region = get_active_aws_region()
146
+ if active_region:
147
+ self._env["AWS_REGION"] = active_region
148
+ self._env["AWS_DEFAULT_REGION"] = active_region
149
+ os.environ["AWS_REGION"] = active_region
150
+ os.environ["AWS_DEFAULT_REGION"] = active_region
151
+ else:
152
+ self._env.pop("AWS_REGION", None)
153
+ self._env.pop("AWS_DEFAULT_REGION", None)
154
+ os.environ.pop("AWS_REGION", None)
155
+ os.environ.pop("AWS_DEFAULT_REGION", None)
156
+
157
+ def execute(
158
+ self,
159
+ command: str,
160
+ *,
161
+ timeout: int | None = None,
162
+ ) -> Any:
163
+ """Execute a shell command with dynamically synchronized cloud profile environment."""
164
+ self._sync_cloud_env()
165
+ return super().execute(command, timeout=timeout)
166
+
167
+ async def aexecute(
168
+ self,
169
+ command: str,
170
+ *,
171
+ timeout: int | None = None,
172
+ ) -> Any:
173
+ """Asynchronously execute shell command with dynamically synchronized cloud profile environment."""
174
+ self._sync_cloud_env()
175
+ return await super().aexecute(command, timeout=timeout)
176
+
177
+
178
+ __all__ = [
179
+ "ALLOWED_ENV_PREFIXES",
180
+ "COMMON_TOOL_DIRS",
181
+ "LocalShellBackend",
182
+ "_build_shell_env",
183
+ ]
@@ -0,0 +1,79 @@
1
+ """Singleton registry for backend providers in OpsCloud."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ import threading
7
+ from typing import Any, TypeVar
8
+
9
+ T = TypeVar("T", bound=type)
10
+
11
+
12
+ class BackendRegistry:
13
+ """Singleton registry for backend providers."""
14
+
15
+ _instance: BackendRegistry | None = None
16
+ _lock = threading.Lock()
17
+
18
+ def __init__(self) -> None:
19
+ """Initialize BackendRegistry with an empty provider mapping."""
20
+ self._providers: dict[str, type] = {}
21
+
22
+ def register(self, name: str, cls: type) -> None:
23
+ """Register a backend provider class under *name*."""
24
+ self._providers[name] = cls
25
+
26
+ def build(self, name: str, **kwargs: Any) -> Any:
27
+ """Instantiate a registered backend by *name*."""
28
+ cls = self._providers.get(name)
29
+ if cls is None:
30
+ raise ValueError(f"Backend provider '{name}' is not registered.")
31
+ return cls(**kwargs)
32
+
33
+ def list_registered(self) -> list[str]:
34
+ """Return sorted list of registered backend names."""
35
+ return sorted(self._providers.keys())
36
+
37
+ @classmethod
38
+ def get_instance(cls) -> BackendRegistry:
39
+ """Return the singleton instance, creating it if needed."""
40
+ if cls._instance is None:
41
+ with cls._lock:
42
+ if cls._instance is None:
43
+ cls._instance = cls()
44
+ return cls._instance
45
+
46
+
47
+ def get_backend_registry() -> BackendRegistry:
48
+ """Module-level accessor for the singleton ``BackendRegistry``."""
49
+ return BackendRegistry.get_instance()
50
+
51
+
52
+ def register_backend(name: str) -> Callable[[T], T]:
53
+ """Class decorator to register a backend provider.
54
+
55
+ Usage::
56
+
57
+ @register_backend("local")
58
+ class LocalShellBackend(SDKLocalShellBackend):
59
+ ...
60
+ """
61
+
62
+ def decorator(cls: T) -> T:
63
+ get_backend_registry().register(name, cls)
64
+ return cls
65
+
66
+ return decorator
67
+
68
+
69
+ def get_backend_class(name: str) -> type | None:
70
+ """Return the registered backend class for *name*, or None."""
71
+ return get_backend_registry()._providers.get(name)
72
+
73
+
74
+ __all__ = [
75
+ "BackendRegistry",
76
+ "get_backend_class",
77
+ "get_backend_registry",
78
+ "register_backend",
79
+ ]
@@ -0,0 +1,107 @@
1
+ """Configuration model for sandboxed backend execution environments in OpsCloud."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ @dataclass
12
+ class SandboxConfig:
13
+ """Settings required to provision, configure, and connect to a sandbox environment."""
14
+
15
+ provider: str = "local"
16
+ """Target provider identifier ('local', 'langsmith', 'daytona', 'modal', 'runloop', 'vercel', 'agentcore')."""
17
+
18
+ sandbox_id: str | None = None
19
+ """Optional existing remote sandbox ID to reconnect to."""
20
+
21
+ image: str | None = None
22
+ """Optional container base image name or Dockerfile ref."""
23
+
24
+ setup_script_path: str | None = None
25
+ """Optional path to a shell script to run inside the sandbox on boot."""
26
+
27
+ snapshot_name: str | None = None
28
+ """Optional snapshot or blueprint name to boot from (supported by LangSmith/Runloop)."""
29
+
30
+ sync_workspace: bool = True
31
+ """Whether to automatically synchronize local project files into the sandbox (default True)."""
32
+
33
+ providers: dict[str, Any] = field(default_factory=dict)
34
+ """Optional provider-specific configurations parsed from [sandboxes.<provider>]."""
35
+
36
+ @classmethod
37
+ def load(cls, config_path: Path | None = None) -> SandboxConfig:
38
+ """Load sandbox configuration with layered precedence: Env > TOML > Defaults."""
39
+ from opscloud.config.toml_config import read_config_toml
40
+
41
+ toml_data = read_config_toml(config_path)
42
+ sandboxes_table = toml_data.get("sandboxes", {})
43
+ if not isinstance(sandboxes_table, dict):
44
+ sandboxes_table = {}
45
+
46
+ # Provider / default
47
+ env_provider = os.getenv("OPSCLOUD_SANDBOX_PROVIDER") or os.getenv("SANDBOX_PROVIDER")
48
+ toml_provider = sandboxes_table.get("default") or sandboxes_table.get("provider")
49
+ provider = env_provider or toml_provider or "local"
50
+
51
+ # Sandbox ID
52
+ env_sandbox_id = os.getenv("OPSCLOUD_SANDBOX_ID") or os.getenv("SANDBOX_ID")
53
+ toml_sandbox_id = sandboxes_table.get("sandbox_id")
54
+ sandbox_id = env_sandbox_id or toml_sandbox_id
55
+
56
+ # Image
57
+ env_image = os.getenv("OPSCLOUD_SANDBOX_IMAGE") or os.getenv("SANDBOX_IMAGE")
58
+ toml_image = sandboxes_table.get("image")
59
+ image = env_image or toml_image
60
+
61
+ # Setup script path
62
+ env_setup_script = os.getenv("OPSCLOUD_SANDBOX_SETUP_SCRIPT") or os.getenv("SANDBOX_SETUP_SCRIPT")
63
+ toml_setup_script = sandboxes_table.get("setup_script_path")
64
+ setup_script_path = env_setup_script or toml_setup_script
65
+
66
+ # Snapshot name
67
+ env_snapshot = os.getenv("OPSCLOUD_SANDBOX_SNAPSHOT") or os.getenv("SANDBOX_SNAPSHOT")
68
+ toml_snapshot = sandboxes_table.get("snapshot_name") or sandboxes_table.get("snapshot")
69
+ snapshot_name = env_snapshot or toml_snapshot
70
+
71
+ # Sync workspace
72
+ sync_raw = os.getenv("OPSCLOUD_SANDBOX_SYNC_WORKSPACE") or os.getenv("SANDBOX_SYNC_WORKSPACE")
73
+ if sync_raw is not None:
74
+ sync_workspace = sync_raw.lower() not in ("false", "0", "no")
75
+ elif "sync_workspace" in sandboxes_table:
76
+ val = sandboxes_table["sync_workspace"]
77
+ sync_workspace = bool(val) if not isinstance(val, str) else val.lower() not in ("false", "0", "no")
78
+ else:
79
+ sync_workspace = True
80
+
81
+ # Extract nested provider tables (e.g. [sandboxes.daytona])
82
+ nested_providers: dict[str, Any] = {}
83
+ for k, v in sandboxes_table.items():
84
+ if isinstance(v, dict):
85
+ nested_providers[k] = v
86
+
87
+ provider_clean = str(provider).lower().strip()
88
+ if provider_clean in ("none", ""):
89
+ provider_clean = "local"
90
+
91
+ return cls(
92
+ provider=provider_clean,
93
+ sandbox_id=str(sandbox_id) if sandbox_id else None,
94
+ image=str(image) if image else None,
95
+ setup_script_path=str(setup_script_path) if setup_script_path else None,
96
+ snapshot_name=str(snapshot_name) if snapshot_name else None,
97
+ sync_workspace=sync_workspace,
98
+ providers=nested_providers,
99
+ )
100
+
101
+ @classmethod
102
+ def from_env(cls) -> SandboxConfig:
103
+ """Load sandbox configuration (delegates to load())."""
104
+ return cls.load()
105
+
106
+
107
+ __all__ = ["SandboxConfig"]