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,432 @@
1
+ # pyright: reportMissingImports=false
2
+ """Factory for creating sandbox backends (LangSmith, Daytona, Modal, Runloop).
3
+
4
+ Note: Sandbox providers (LangSmith, Daytona, Modal, Runloop, Vercel) are optional
5
+ dependencies and are imported lazily inside their respective creation functions
6
+ to avoid hard dependency requirements at startup.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import atexit
12
+ import os
13
+ from pathlib import Path
14
+ import shlex
15
+ import string
16
+ import threading
17
+ from typing import Any
18
+
19
+ from opscloud.backend.local import LocalShellBackend
20
+ from opscloud.backend.sandbox_config import SandboxConfig
21
+ from opscloud.config.paths import find_project_root
22
+ from opscloud.utils.logger import AgentLogger
23
+
24
+ logger = AgentLogger("SandboxFactory")
25
+
26
+ # Thread-safe global cache for active sandbox backends, keyed by thread_id
27
+ _active_sandboxes: dict[str, Any] = {}
28
+ _active_sandboxes_lock = threading.Lock()
29
+
30
+ # Registry of active context managers or providers to delete at exit
31
+ _active_providers: dict[str, Any] = {}
32
+
33
+ # Default working directories inside the sandbox containers
34
+ WORKING_DIRS = {
35
+ "langsmith": "/root",
36
+ "modal": "/workspace",
37
+ "daytona": "/home/daytona",
38
+ "runloop": "/home/user",
39
+ "vercel": "/vercel/sandbox",
40
+ "agentcore": "/workspace",
41
+ }
42
+
43
+ # Directories to exclude from remote sandbox file synchronization
44
+ WORKSPACE_IGNORE_DIRS: frozenset[str] = frozenset(
45
+ {
46
+ ".git",
47
+ ".venv",
48
+ "__pycache__",
49
+ ".pytest_cache",
50
+ ".mypy_cache",
51
+ ".gemini",
52
+ ".agents",
53
+ ".opscloud",
54
+ "tests",
55
+ "scratch",
56
+ ".terraform",
57
+ ".terragrunt-cache",
58
+ "cdk.out",
59
+ "node_modules",
60
+ "dist",
61
+ "build",
62
+ }
63
+ )
64
+
65
+ WORKSPACE_IGNORE_EXTS: frozenset[str] = frozenset(
66
+ {
67
+ ".pyc",
68
+ ".pyo",
69
+ ".pyd",
70
+ ".bak",
71
+ ".tmp",
72
+ }
73
+ )
74
+
75
+
76
+ def get_sandbox_working_dir(provider: str) -> str:
77
+ """Return default working directory for a given provider."""
78
+ return WORKING_DIRS.get(provider, "/workspace")
79
+
80
+
81
+ def get_project_root() -> Path:
82
+ """Return the resolved project root or current working directory."""
83
+ root = find_project_root()
84
+ if root is not None:
85
+ return root
86
+ try:
87
+ from opscloud.project_utils import get_server_project_context
88
+
89
+ ctx = get_server_project_context()
90
+ if ctx is not None:
91
+ return ctx.user_cwd
92
+ except Exception:
93
+ pass
94
+ return Path.cwd().resolve()
95
+
96
+
97
+ def _sync_workspace_to_sandbox(backend: Any, provider: str) -> None:
98
+ """Upload project workspace files to the remote sandbox container."""
99
+ root = get_project_root()
100
+ working_dir = get_sandbox_working_dir(provider)
101
+ logger.info(f"Syncing workspace {root} to sandbox {working_dir}...")
102
+
103
+ files_to_upload: list[tuple[str, bytes]] = []
104
+ for dirpath, dirnames, filenames in os.walk(root):
105
+ # Prune ignored directories in-place
106
+ dirnames[:] = [d for d in dirnames if d not in WORKSPACE_IGNORE_DIRS]
107
+
108
+ for fname in filenames:
109
+ ext = os.path.splitext(fname)[1].lower()
110
+ if ext in WORKSPACE_IGNORE_EXTS:
111
+ continue
112
+
113
+ local_path = Path(dirpath) / fname
114
+ try:
115
+ # Calculate relative path and target absolute path in the sandbox
116
+ rel_path = local_path.relative_to(root)
117
+ remote_path = f"{working_dir}/{rel_path}"
118
+
119
+ # Check file size (skip large files > 10MB)
120
+ file_size = local_path.stat().st_size
121
+ if file_size > 10 * 1024 * 1024:
122
+ logger.debug(f"Skipping large file during sync: {rel_path} ({file_size} bytes)")
123
+ continue
124
+
125
+ content = local_path.read_bytes()
126
+ files_to_upload.append((remote_path, content))
127
+ except Exception as e:
128
+ logger.warning(f"Failed to prepare file {local_path} for upload: {e}")
129
+
130
+ # Upload files in batches to prevent hitting request payload or token limits
131
+ batch_size = 20
132
+ logger.info(f"Uploading {len(files_to_upload)} files to sandbox...")
133
+ for i in range(0, len(files_to_upload), batch_size):
134
+ batch = files_to_upload[i : i + batch_size]
135
+ try:
136
+ responses = backend.upload_files(batch)
137
+ for resp in responses:
138
+ if getattr(resp, "error", None):
139
+ logger.warning(f"Failed to upload {resp.path}: {resp.error}")
140
+ except Exception as e:
141
+ logger.error(f"Error uploading batch {i // batch_size + 1}: {e}")
142
+
143
+ logger.info("Workspace synchronization completed.")
144
+
145
+
146
+ def _run_sandbox_setup(backend: Any, setup_script_path: str) -> None:
147
+ """Run user's setup script inside the sandbox after expanding environment variables."""
148
+ script_path = Path(setup_script_path)
149
+ if not script_path.exists():
150
+ raise FileNotFoundError(f"Setup script not found at {setup_script_path}")
151
+
152
+ logger.info(f"Executing setup script: {setup_script_path}...")
153
+ script_content = script_path.read_text(encoding="utf-8")
154
+
155
+ # Substitute environment variables
156
+ template = string.Template(script_content)
157
+ expanded_script = template.safe_substitute(os.environ)
158
+
159
+ # Run in sandbox
160
+ result = backend.execute(f"bash -c {shlex.quote(expanded_script)}")
161
+ if result.exit_code != 0:
162
+ logger.error(f"Setup script failed with exit code {result.exit_code}:\n{result.output}")
163
+ raise RuntimeError(f"Sandbox setup script execution failed: exit {result.exit_code}")
164
+
165
+ logger.info("Sandbox setup complete.")
166
+
167
+
168
+ def _create_langsmith_sandbox(config: SandboxConfig) -> Any:
169
+ """Create a LangSmith sandbox backend."""
170
+ try:
171
+ from deepagents.backends.langsmith import (
172
+ LangSmithSandbox, # Lazy import: optional dependency
173
+ )
174
+ from langsmith.sandbox import SandboxClient # Lazy import: optional dependency
175
+ except ImportError as e:
176
+ raise ImportError(
177
+ "LangSmith sandbox dependencies not installed. Install them to use LangSmith sandboxes."
178
+ ) from e
179
+
180
+ api_key = (
181
+ os.getenv("OPSCLOUD_LANGSMITH_API_KEY")
182
+ or os.getenv("LANGSMITH_SANDBOX_API_KEY")
183
+ or os.getenv("LANGSMITH_API_KEY")
184
+ or os.getenv("LANGCHAIN_API_KEY")
185
+ )
186
+ if not api_key:
187
+ raise ValueError("No LangSmith API key found. Set LANGSMITH_API_KEY.")
188
+
189
+ client = SandboxClient(api_key=api_key)
190
+
191
+ if config.sandbox_id:
192
+ logger.info(f"Connecting to existing LangSmith sandbox ID: {config.sandbox_id}")
193
+ sandbox = client.get_sandbox(name=config.sandbox_id)
194
+ return LangSmithSandbox(sandbox)
195
+
196
+ snapshot_name = config.snapshot_name or "opscloud-aws-agent"
197
+ image = config.image or "python:3.12-slim"
198
+
199
+ logger.info(f"Ensuring LangSmith snapshot ready: {snapshot_name} (image: {image})")
200
+ snapshots = client.list_snapshots()
201
+ snapshot_id = None
202
+ for snap in snapshots:
203
+ if snap.name == snapshot_name and snap.status == "ready":
204
+ snapshot_id = snap.id
205
+ break
206
+
207
+ if not snapshot_id:
208
+ logger.info(f"Building new LangSmith snapshot: {snapshot_name}")
209
+ snapshot = client.create_snapshot(
210
+ name=snapshot_name,
211
+ docker_image=image,
212
+ fs_capacity_bytes=16 * 1024**3,
213
+ )
214
+ snapshot_id = snapshot.id
215
+
216
+ logger.info(f"Creating fresh LangSmith sandbox from snapshot {snapshot_name}...")
217
+ sandbox = client.create_sandbox(snapshot_id=snapshot_id)
218
+ return LangSmithSandbox(sandbox)
219
+
220
+
221
+ def _import_provider_module(
222
+ module_name: str,
223
+ *,
224
+ provider: str,
225
+ package: str,
226
+ ) -> Any:
227
+ """Import an optional provider module with a clear install command.
228
+
229
+ Args:
230
+ module_name: Python module name to import.
231
+ provider: Sandbox provider name (e.g. 'daytona').
232
+ package: PyPI package name to install if missing.
233
+
234
+ Returns:
235
+ The imported module object.
236
+
237
+ Raises:
238
+ ImportError: If the optional dependency is not installed.
239
+ """
240
+ import importlib
241
+
242
+ try:
243
+ return importlib.import_module(module_name)
244
+ except ImportError as exc:
245
+ msg = (
246
+ f"The '{provider}' sandbox provider requires the '{package}' package. "
247
+ f"Install it with: uv pip install {package}"
248
+ )
249
+ raise ImportError(msg) from exc
250
+
251
+
252
+ def _create_other_sandbox(config: SandboxConfig) -> Any:
253
+ """Dynamic fallback provider loader for Daytona, Modal, Runloop, Vercel."""
254
+ provider = config.provider
255
+ if provider == "daytona":
256
+ daytona = _import_provider_module("daytona", provider="daytona", package="daytona")
257
+ langchain_daytona = _import_provider_module("langchain_daytona", provider="daytona", package="langchain-daytona")
258
+ DaytonaSandbox = getattr(langchain_daytona, "DaytonaSandbox")
259
+
260
+ api_key = os.getenv("DAYTONA_API_KEY")
261
+ if not api_key:
262
+ raise ValueError("No Daytona API key found. Set DAYTONA_API_KEY.")
263
+ client = daytona.Daytona(
264
+ daytona.DaytonaConfig(
265
+ api_key=api_key,
266
+ api_url=os.getenv("DAYTONA_API_URL"),
267
+ )
268
+ )
269
+ sandbox = client.create()
270
+ return DaytonaSandbox(sandbox=sandbox)
271
+
272
+ elif provider == "modal":
273
+ modal = _import_provider_module("modal", provider="modal", package="modal")
274
+ langchain_modal = _import_provider_module("langchain_modal", provider="modal", package="langchain-modal")
275
+ ModalSandbox = getattr(langchain_modal, "ModalSandbox")
276
+
277
+ app = modal.App.lookup(name="opscloud-sandbox", create_if_missing=True)
278
+ if config.sandbox_id:
279
+ sandbox = modal.Sandbox.from_id(sandbox_id=config.sandbox_id)
280
+ else:
281
+ sandbox = modal.Sandbox.create(app=app, workdir="/workspace")
282
+ return ModalSandbox(sandbox=sandbox)
283
+
284
+ elif provider == "runloop":
285
+ langchain_runloop = _import_provider_module("langchain_runloop", provider="runloop", package="langchain-runloop")
286
+ RunloopProvider = getattr(langchain_runloop, "RunloopProvider")
287
+
288
+ api_key = os.getenv("RUNLOOP_API_KEY")
289
+ if not api_key:
290
+ raise ValueError("No Runloop API key found. Set RUNLOOP_API_KEY.")
291
+ prov = RunloopProvider(api_key=api_key)
292
+ return prov.get_or_create(sandbox_id=config.sandbox_id, blueprint=config.snapshot_name)
293
+
294
+ elif provider == "agentcore":
295
+ from opscloud.config.aws import get_boto3_session
296
+
297
+ agentcore_module = _import_provider_module(
298
+ "bedrock_agentcore.tools.code_interpreter_client",
299
+ provider="agentcore",
300
+ package="langchain-agentcore-codeinterpreter",
301
+ )
302
+ agentcore_backend = _import_provider_module(
303
+ "langchain_agentcore_codeinterpreter",
304
+ provider="agentcore",
305
+ package="langchain-agentcore-codeinterpreter",
306
+ )
307
+ sess = get_boto3_session()
308
+ region = sess.region_name or "us-west-2"
309
+ CodeInterpreter = getattr(agentcore_module, "CodeInterpreter")
310
+ AgentCoreSandbox = getattr(agentcore_backend, "AgentCoreSandbox")
311
+ interpreter = CodeInterpreter(region=region, session=sess)
312
+ interpreter.start()
313
+ return AgentCoreSandbox(interpreter=interpreter)
314
+
315
+ else:
316
+ raise NotImplementedError(f"Unsupported sandbox provider: {provider}")
317
+
318
+
319
+ def get_sandbox_backend(thread_id: str | None = None) -> Any:
320
+ """Resolve, create, or fetch the cached sandbox backend for the given session thread."""
321
+ config = SandboxConfig.from_env()
322
+
323
+ # If provider is local, bypass the cache and return a local shell backend immediately
324
+ if config.provider == "local":
325
+ root = get_project_root()
326
+ return LocalShellBackend(
327
+ root_dir=root,
328
+ virtual_mode=False,
329
+ inherit_env=True,
330
+ )
331
+
332
+ # Use thread_id or fallback to a global key for execution loops without a session graph context
333
+ cache_key = thread_id or "__global__"
334
+
335
+ with _active_sandboxes_lock:
336
+ if cache_key in _active_sandboxes:
337
+ return _active_sandboxes[cache_key]
338
+
339
+ logger.info(f"Initializing new sandbox backend for key {cache_key} (provider: {config.provider})...")
340
+
341
+ backend = (
342
+ _create_langsmith_sandbox(config)
343
+ if config.provider == "langsmith"
344
+ else _create_other_sandbox(config)
345
+ )
346
+
347
+ # Synchronize workspace if enabled
348
+ if config.sync_workspace:
349
+ try:
350
+ _sync_workspace_to_sandbox(backend, config.provider)
351
+ except Exception as e:
352
+ logger.error(f"Workspace sync to sandbox failed: {e}")
353
+
354
+ # Run setup script if specified
355
+ if config.setup_script_path:
356
+ try:
357
+ _run_sandbox_setup(backend, config.setup_script_path)
358
+ except Exception as e:
359
+ logger.error(f"Sandbox setup script failed: {e}")
360
+
361
+ # Cache the initialized backend
362
+ _active_sandboxes[cache_key] = backend
363
+ _active_providers[cache_key] = (config.provider, getattr(backend, "id", str(cache_key)))
364
+
365
+ logger.info(f"Sandbox backend successfully cached. Key: {cache_key}")
366
+ return backend
367
+
368
+
369
+ def cleanup_sandbox(thread_id: str) -> None:
370
+ """Terminate the sandbox backend associated with a thread ID."""
371
+ with _active_sandboxes_lock:
372
+ backend = _active_sandboxes.pop(thread_id, None)
373
+ provider_info = _active_providers.pop(thread_id, None)
374
+
375
+ if backend and provider_info:
376
+ provider, sandbox_id = provider_info
377
+ logger.info(f"Terminating sandbox {sandbox_id} ({provider}) for thread {thread_id}...")
378
+ try:
379
+ if provider == "langsmith":
380
+ from langsmith.sandbox import SandboxClient
381
+
382
+ client = SandboxClient()
383
+ client.delete_sandbox(sandbox_id)
384
+ elif provider == "daytona":
385
+ daytona = _import_provider_module("daytona", provider="daytona", package="daytona")
386
+ client = daytona.Daytona()
387
+ sandbox = client.get(sandbox_id)
388
+ client.delete(sandbox)
389
+ elif provider == "modal":
390
+ modal = _import_provider_module("modal", provider="modal", package="modal")
391
+ sandbox = modal.Sandbox.from_id(sandbox_id=sandbox_id)
392
+ sandbox.terminate()
393
+ elif provider == "runloop":
394
+ langchain_runloop = _import_provider_module("langchain_runloop", provider="runloop", package="langchain-runloop")
395
+ RunloopProvider = getattr(langchain_runloop, "RunloopProvider")
396
+ api_key = os.getenv("RUNLOOP_API_KEY")
397
+ if not api_key:
398
+ logger.warning("No Runloop API key found during cleanup. Runloop sandbox cannot be deleted.")
399
+ else:
400
+ prov = RunloopProvider(api_key=api_key)
401
+ prov.delete(sandbox_id=sandbox_id)
402
+ logger.info(f"Sandbox {sandbox_id} terminated.")
403
+ except Exception as e:
404
+ logger.warning(f"Error during sandbox termination for {sandbox_id}: {e}")
405
+
406
+
407
+ def cleanup_all_sandboxes() -> None:
408
+ """Cleanup exit handler to shut down all active sandboxes on process shutdown."""
409
+ keys = list(_active_sandboxes.keys())
410
+ if keys:
411
+ logger.info("Process exit detected. Cleaning up all active sandboxes...")
412
+ for key in keys:
413
+ cleanup_sandbox(key)
414
+
415
+
416
+ # Register exit handler
417
+ atexit.register(cleanup_all_sandboxes)
418
+
419
+ create_sandbox_backend = get_sandbox_backend
420
+ clear_sandbox_cache = cleanup_all_sandboxes
421
+
422
+ __all__ = [
423
+ "WORKSPACE_IGNORE_DIRS",
424
+ "WORKSPACE_IGNORE_EXTS",
425
+ "clear_sandbox_cache",
426
+ "cleanup_all_sandboxes",
427
+ "cleanup_sandbox",
428
+ "create_sandbox_backend",
429
+ "get_project_root",
430
+ "get_sandbox_backend",
431
+ "get_sandbox_working_dir",
432
+ ]
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: remember
3
+ description: "Distill the conversation to capture cloud infrastructure decisions, DevOps patterns, operator preferences, and operational runbooks into persistent memory (AGENTS.md) or reusable OpsCloud skills. Use when the operator says: (1) remember this, (2) save cloud preferences, (3) update memory, (4) persist operational learnings."
4
+ license: MIT
5
+ compatibility: designed for opscloud
6
+ ---
7
+
8
+ Review the conversation and capture valuable operational knowledge. Focus especially on **cloud & DevOps best practices**, **infrastructure conventions**, and **operator preferences**—these are the most important things to preserve across sessions.
9
+
10
+ ## Step 1: Identify Best Practices and Key Learnings
11
+
12
+ Scan the conversation for:
13
+
14
+ ### Cloud Infrastructure & DevOps Practices (highest priority)
15
+ - **Patterns that worked well** - cloud architectures, Terraform modules, Helm values, Kubernetes configurations, and deployment pipelines proven effective
16
+ - **Gotchas and anti-patterns to avoid** - cloud misconfigurations, IAM privilege pitfalls, security group leaks, egress cost surprises, or brittle CLI commands
17
+ - **Operational & reliability standards** - SLAs, backup policies, tagging conventions, health check setups, and disaster recovery procedures
18
+ - **Architectural decision rationale** - why specific instance types (e.g. Graviton), regions, cloud providers, or networking topologies were selected
19
+
20
+ ### Operator & Environment Context
21
+ - Operator personal details (names, family context, preferences) and communication style
22
+ - Active cloud profiles (e.g., AWS profiles like `krayak`), default accounts, and preferred regions
23
+ - Project infrastructure layout, workspace paths, and toolchain versions
24
+ - Operator feedback on agent behavior, automated remediation steps, or approval thresholds
25
+
26
+ ## Step 2: Decide Where to Store Each Learning
27
+
28
+ For each learning or convention, choose the appropriate destination:
29
+
30
+ ### -> Persistent Memory (AGENTS.md) for preferences, conventions, and rules
31
+ Use persistent memory when the knowledge is:
32
+ - An operator preference, personal detail, or environment default
33
+ - A global or project cloud convention (e.g., "always deploy to us-west-2", "tag all AWS resources with CostCenter")
34
+ - A rule or constraint the agent must always respect
35
+
36
+ **Global** (`$OPSCLOUD_HOME/AGENTS.md` or `~/.opscloud/AGENTS.md`): Universal operator identity, personal details, cross-cloud defaults, and global DevOps preferences
37
+ **Project** (`.opscloud/AGENTS.md` or `AGENTS.md`): Project-specific infrastructure conventions, cloud accounts, Terraform workspace rules, and cluster topologies
38
+
39
+ ### -> Reusable Skill for multi-step DevOps workflows and runbooks
40
+ **Create a skill when** we developed:
41
+ - A multi-step operational runbook or deployment workflow
42
+ - A methodology for troubleshooting complex cloud incidents (e.g., EKS node drain failures, VPC peering issues)
43
+ - An infrastructure automation pattern with safety checks and best practices baked in
44
+ - A procedure that should be executed consistently
45
+
46
+ Skills are more powerful than memory entries because they can encode **how** to execute operational procedures safely, not just **what** to remember.
47
+
48
+ ## Step 3: Create Skills for Significant Operational Procedures
49
+
50
+ If we established best practices around a cloud workflow or runbook, capture them in a reusable skill.
51
+
52
+ **Example:** If we established a standard procedure for provisioning EKS clusters with Karpenter, rotating IAM credentials, or triaging S3 lifecycle rules, create an OpsCloud skill that encodes those practices into a structured workflow.
53
+
54
+ ### Skill Location
55
+ - Global: `$OPSCLOUD_HOME/skills/<skill-name>/SKILL.md` (or `~/.opscloud/skills/<skill-name>/SKILL.md`)
56
+ - Project: `.opscloud/skills/<skill-name>/SKILL.md`
57
+
58
+ ### Skill Structure
59
+ ```
60
+ skill-name/
61
+ ├── SKILL.md (required - main instructions with best practices)
62
+ ├── scripts/ (optional - automation scripts)
63
+ ├── references/ (optional - cloud specs, architecture docs)
64
+ └── assets/ (optional - templates, policy examples)
65
+ ```
66
+
67
+ ### SKILL.md Format
68
+ ```markdown
69
+ ---
70
+ name: skill-name
71
+ description: "What this cloud skill does AND when to activate it. Include specific triggers like 'when deploying to EKS' or 'when configuring AWS VPC'."
72
+ ---
73
+
74
+ # Skill Name
75
+
76
+ ## Overview
77
+ Brief explanation of what this cloud skill accomplishes.
78
+
79
+ ## Best Practices
80
+ Capture the key cloud & DevOps practices upfront:
81
+ - Best practice 1: explanation
82
+ - Best practice 2: explanation
83
+
84
+ ## Operational Procedure
85
+ Step-by-step instructions (imperative form):
86
+ 1. First, verify active cloud credentials and target account
87
+ 2. Next, validate configuration with dry-run or plan
88
+ 3. Apply changes and verify health metrics
89
+
90
+ ## Common Pitfalls
91
+ - Pitfall to avoid and why
92
+ - Anti-pattern discovered during past operations
93
+ ```
94
+
95
+ ### Key Principles
96
+ 1. **Encode safety & best practices prominently** - Put credential checks and dry-run steps near the top
97
+ 2. **Concise is key** - Only include non-obvious knowledge; justify every paragraph's token cost
98
+ 3. **Clear triggers** - The description determines when OpsCloud activates the skill; be specific
99
+ 4. **Imperative form** - Write commands: "Verify cluster status" not "You should verify cluster status"
100
+ 5. **Include anti-patterns** - What NOT to do in production is often as critical as what to do
101
+
102
+ ## Step 4: Update Persistent Memory for Simpler Learnings
103
+
104
+ For operator preferences, identity details, and DevOps guidelines that don't warrant a full skill:
105
+
106
+ ```markdown
107
+ ## Operator Preferences & Identity
108
+ - Preferred name: Sandeep
109
+ - Personal / family context: ...
110
+ - Preferred AWS region: us-west-2
111
+ - Always use Graviton (arm64) instances for cost efficiency
112
+
113
+ ## Infrastructure Standards & Conventions
114
+ - Enforce tags: Environment, Owner, ManagedBy=OpsCloud
115
+ - Avoid public S3 buckets unless explicitly authorized
116
+ ```
117
+
118
+ Use `read_file` to inspect the target `AGENTS.md` (e.g. `~/.opscloud/AGENTS.md`) first. Check if the section or preference already exists. Then use `edit_file` to update/insert, or `write_file` if creating anew. Preserve existing markers and structure.
119
+
120
+ ## Step 5: Summarize Changes
121
+
122
+ List what was captured and where it was stored:
123
+ - Skills created (with key operational practices encoded)
124
+ - Memory entries added (with location e.g. `Saved to persistent memory (~/.opscloud/AGENTS.md):`)