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,90 @@
1
+ """Middleware for enforcing shell command allow-lists."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from langchain.agents.middleware.types import AgentMiddleware
9
+ from langchain_core.messages import ToolMessage
10
+ from langgraph.prebuilt.tool_node import ToolCallRequest
11
+
12
+ from opscloud.middleware.registry import register_middleware
13
+ from opscloud.security.shell_safety import is_shell_command_allowed
14
+ from opscloud.utils.logger import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ @register_middleware(name="shell_allow_list")
20
+ class ShellAllowListMiddleware(AgentMiddleware[Any, Any]):
21
+ """Validate shell commands against an allow-list without HITL interrupts."""
22
+
23
+ def __init__(self, allow_list: list[str] | None = None) -> None:
24
+ """Initialize ShellAllowListMiddleware.
25
+
26
+ Args:
27
+ allow_list: Optional list of allowed shell executable binary names or command prefixes.
28
+ """
29
+ super().__init__()
30
+ self._allow_list = allow_list or []
31
+
32
+ def _validate_tool_call(self, request: ToolCallRequest) -> ToolMessage | None:
33
+ if not getattr(request, "tool_call", None) or not isinstance(request.tool_call, dict):
34
+ return None
35
+
36
+ tool_name = request.tool_call.get("name")
37
+ if tool_name not in {"execute", "run_command"}:
38
+ return None
39
+
40
+ args = request.tool_call.get("args") or {}
41
+ command = args.get("command") or args.get("CommandLine") or ""
42
+ if not self._allow_list:
43
+ return ToolMessage(
44
+ content="Shell command rejected: no commands are allowed in this mode.",
45
+ name=str(tool_name),
46
+ tool_call_id=str(request.tool_call.get("id") or ""),
47
+ status="error",
48
+ )
49
+
50
+ if is_shell_command_allowed(command, self._allow_list):
51
+ logger.debug("Shell command allowed: %r", command)
52
+ return None
53
+
54
+ logger.warning("Shell command rejected by allow-list: %r", command)
55
+ allowed_str = ", ".join(self._allow_list)
56
+ return ToolMessage(
57
+ content=(
58
+ f"Shell command rejected: `{command}` is not in the allow-list. "
59
+ f"Allowed commands: {allowed_str}. "
60
+ f"Please use an allowed command or try another approach."
61
+ ),
62
+ name=str(tool_name),
63
+ tool_call_id=str(request.tool_call.get("id") or ""),
64
+ status="error",
65
+ )
66
+
67
+ def wrap_tool_call(
68
+ self,
69
+ request: ToolCallRequest,
70
+ handler: Callable[[ToolCallRequest], Any],
71
+ ) -> Any:
72
+ """Wrap synchronous tool call and block disallowed shell commands."""
73
+ err = self._validate_tool_call(request)
74
+ if err is not None:
75
+ return err
76
+ return handler(request)
77
+
78
+ async def awrap_tool_call(
79
+ self,
80
+ request: ToolCallRequest,
81
+ handler: Callable[[ToolCallRequest], Any],
82
+ ) -> Any:
83
+ """Wrap asynchronous tool call and block disallowed shell commands."""
84
+ err = self._validate_tool_call(request)
85
+ if err is not None:
86
+ return err
87
+ return await handler(request)
88
+
89
+
90
+ __all__ = ["ShellAllowListMiddleware"]
@@ -0,0 +1,382 @@
1
+ """Skills middleware adapter supporting namespaced plugin skills."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Sequence
7
+ import fnmatch
8
+ from pathlib import Path, PurePosixPath
9
+ from typing import TYPE_CHECKING, Any, cast
10
+
11
+ from deepagents.backends.protocol import FileInfo, LsResult
12
+ from deepagents.backends.utils import to_posix_path
13
+ from deepagents.middleware import skills as sdk_skills
14
+ from deepagents.middleware.skills import SkillsMiddleware
15
+
16
+ from opscloud.middleware.registry import register_middleware
17
+ from opscloud.plugins.adapters.skills import (
18
+ SkillNamespace,
19
+ namespaced_skill_name,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from deepagents.backends.protocol import BackendProtocol
24
+ from langchain_core.runnables import RunnableConfig
25
+ from langgraph.runtime import Runtime
26
+ from opscloud.skills.registry import SkillSourceTuple
27
+ from opscloud.skills.sources import CodeSkillSource
28
+
29
+ from opscloud.utils.logger import get_logger
30
+
31
+ logger = get_logger(__name__)
32
+
33
+ _PLUGIN_SKILL_SOURCE_LENGTH = 3
34
+ _SKILL_FILE = "SKILL.md"
35
+
36
+
37
+ def _entries(ls_result: object) -> list[FileInfo]:
38
+ if isinstance(ls_result, LsResult):
39
+ return list(ls_result.entries or [])
40
+ if isinstance(ls_result, list):
41
+ return cast(list[FileInfo], ls_result)
42
+ return []
43
+
44
+
45
+ def _child_dirs(entries: list[FileInfo], root: str) -> list[tuple[str, str]]:
46
+ root_posix = PurePosixPath(to_posix_path(root))
47
+ dirs: list[tuple[str, str]] = []
48
+ for entry in entries:
49
+ if not entry.get("is_dir"):
50
+ continue
51
+ path = entry["path"]
52
+ name = PurePosixPath(to_posix_path(path)).name
53
+ if PurePosixPath(to_posix_path(path)) == root_posix:
54
+ continue
55
+ dirs.append((name, path))
56
+ return dirs
57
+
58
+
59
+ def _has_skill_file(entries: list[FileInfo], root: str) -> bool:
60
+ root_posix = PurePosixPath(to_posix_path(root))
61
+ for entry in entries:
62
+ path = PurePosixPath(to_posix_path(entry["path"]))
63
+ if path.name == _SKILL_FILE and path.parent == root_posix:
64
+ return True
65
+ return False
66
+
67
+
68
+ def _skill_md_path(skill_dir: str) -> str:
69
+ return str(PurePosixPath(to_posix_path(skill_dir)) / _SKILL_FILE)
70
+
71
+
72
+ def _namespace_skill(
73
+ skill: sdk_skills.SkillMetadata,
74
+ namespace: SkillNamespace,
75
+ subfolders: tuple[str, ...],
76
+ ) -> sdk_skills.SkillMetadata:
77
+ return cast(
78
+ sdk_skills.SkillMetadata,
79
+ {
80
+ **skill,
81
+ "name": namespaced_skill_name(namespace, skill["name"], subfolders),
82
+ },
83
+ )
84
+
85
+
86
+ def discover_skill_dirs(
87
+ backend: BackendProtocol,
88
+ source_path: str,
89
+ ) -> list[tuple[str, tuple[str, ...]]]:
90
+ """Recursively search for directories containing a SKILL.md definition under source_path.
91
+
92
+ Args:
93
+ backend: Storage backend providing directory listing capabilities.
94
+ source_path: Root filesystem path to search.
95
+
96
+ Returns:
97
+ List of tuples mapping resolved skill directory path to relative path segments.
98
+ """
99
+ found: list[tuple[str, tuple[str, ...]]] = []
100
+ source_root = Path(source_path).resolve()
101
+ visited: set[Path] = set()
102
+ stack: list[tuple[str, tuple[str, ...]]] = [(str(source_root), ())]
103
+ while stack:
104
+ current, path_segments = stack.pop()
105
+ try:
106
+ resolved = Path(current).resolve()
107
+ except (OSError, RuntimeError):
108
+ logger.warning("Could not resolve plugin skill directory %s", current)
109
+ continue
110
+ if not resolved.is_relative_to(source_root) or resolved in visited:
111
+ continue
112
+ visited.add(resolved)
113
+ resolved_path = str(resolved)
114
+ entries = _entries(backend.ls(resolved_path))
115
+ if _has_skill_file(entries, resolved_path):
116
+ found.append((resolved_path, path_segments[:-1]))
117
+ continue
118
+ for name, path in _child_dirs(entries, resolved_path):
119
+ stack.append((path, (*path_segments, name)))
120
+ return found
121
+
122
+
123
+ def load_namespaced_skills(
124
+ backend: BackendProtocol,
125
+ source_path: str,
126
+ namespace: SkillNamespace,
127
+ ) -> list[sdk_skills.SkillMetadata]:
128
+ """Discover and parse all skill definitions within a source root under a given namespace.
129
+
130
+ Args:
131
+ backend: Storage backend providing file reading and listing.
132
+ source_path: Root directory path containing skills.
133
+ namespace: Skill namespace identifier for prefixing skill names.
134
+
135
+ Returns:
136
+ List of loaded and namespaced SkillMetadata objects.
137
+ """
138
+ skill_dirs = discover_skill_dirs(backend, source_path)
139
+ if not skill_dirs:
140
+ return []
141
+ paths = [_skill_md_path(skill_dir) for skill_dir, _ in skill_dirs]
142
+ responses = backend.download_files(paths)
143
+ skills: list[sdk_skills.SkillMetadata] = []
144
+ for (skill_dir, segments), path, response in zip(skill_dirs, paths, responses, strict=True):
145
+ skill = sdk_skills._skill_metadata_from_response(response, skill_dir, path)
146
+ if skill is not None:
147
+ skills.append(_namespace_skill(skill, namespace, segments))
148
+ return skills
149
+
150
+
151
+ CRITERIA_SKILLS_SYSTEM_PROMPT = """## Skills System & Domain Capabilities
152
+
153
+ OpsCloud operates with a skills library providing domain knowledge and specialized cloud operations procedures. Use these capabilities to understand what criteria and verification targets are achievable:
154
+ {skills_locations}{skills_load_warnings}
155
+ **Available Skills:**
156
+
157
+ {skills_list}"""
158
+
159
+
160
+ @register_middleware(name="skills")
161
+ class PluginSkillsMiddleware(SkillsMiddleware):
162
+ """Load namespaced plugin skills with optional whitelist filtering for subagent scoping."""
163
+
164
+ def __init__(
165
+ self,
166
+ *,
167
+ backend: BackendProtocol | None = None,
168
+ sources: Sequence[CodeSkillSource | SkillSourceTuple | tuple[str, ...]] | None = None,
169
+ skill_sources: Sequence[Any] | None = None,
170
+ system_prompt: str | None = None,
171
+ allowed_skills: Sequence[str] | None = None,
172
+ include_subagent_skills: bool = False,
173
+ subagents: Sequence[Any] | None = None,
174
+ planning_mode: bool = False,
175
+ project_root: Path | str | None = None,
176
+ ) -> None:
177
+ """Initialize PluginSkillsMiddleware with backend, sources, and filtering options.
178
+
179
+ Args:
180
+ backend: Storage backend instance.
181
+ sources: Custom skill root sources.
182
+ skill_sources: Structured skill source definitions.
183
+ system_prompt: Custom system prompt template for skill injection.
184
+ allowed_skills: Optional whitelist of skill names permitted for execution.
185
+ include_subagent_skills: Whether to inherit subagent-specific skills.
186
+ subagents: Sequence of subagents whose skills to discover.
187
+ planning_mode: Whether planning mode is active.
188
+ project_root: Optional project root path for skill discovery.
189
+ """
190
+ self._planning_mode = planning_mode
191
+ self._project_root = Path(project_root) if project_root else None
192
+ if system_prompt is None:
193
+ system_prompt = CRITERIA_SKILLS_SYSTEM_PROMPT if planning_mode else sdk_skills.SKILLS_SYSTEM_PROMPT
194
+
195
+ if backend is None:
196
+ from deepagents.backends.filesystem import FilesystemBackend
197
+
198
+ backend = FilesystemBackend(virtual_mode=False)
199
+
200
+ self._dynamic_sources = sources is None and skill_sources is None
201
+ self._include_subagent_skills = include_subagent_skills
202
+ self._subagents = subagents
203
+ if sources is None:
204
+ if skill_sources is not None:
205
+ sources = [(str(getattr(s, "path", s)), getattr(s, "name", str(s))) for s in skill_sources]
206
+ else:
207
+ from opscloud.skills.sources import get_skill_sources
208
+
209
+ sources = get_skill_sources(
210
+ project_root=self._project_root,
211
+ include_subagent_skills=include_subagent_skills,
212
+ subagents=subagents,
213
+ )
214
+
215
+ sdk_sources = [(source[0], source[1]) for source in sources]
216
+ super().__init__(
217
+ backend=backend,
218
+ sources=sdk_sources,
219
+ system_prompt=system_prompt,
220
+ )
221
+ self._namespaces = tuple(
222
+ source[2] if len(source) == _PLUGIN_SKILL_SOURCE_LENGTH else None for source in sources
223
+ )
224
+ self._allowed_skills = tuple(allowed_skills) if allowed_skills is not None else None
225
+
226
+ def _format_skills_locations(self) -> str:
227
+ """Format skills locations for display in system prompt."""
228
+ if self._planning_mode:
229
+ return ""
230
+ return super()._format_skills_locations()
231
+
232
+ def _format_skills_load_warnings(self, errors: list[str]) -> str:
233
+ """Format skills load warnings for display in system prompt."""
234
+ if self._planning_mode:
235
+ return ""
236
+ return super()._format_skills_load_warnings(errors)
237
+
238
+ def _format_skills_list(self, skills: list[sdk_skills.SkillMetadata]) -> str:
239
+ """Format skills metadata cleanly when in planning mode without filesystem paths."""
240
+ if self._planning_mode:
241
+ if not skills:
242
+ return "(No specialized skills currently loaded.)"
243
+ lines: list[str] = []
244
+ for skill in skills:
245
+ name = skill.get("name", "")
246
+ desc = skill.get("description", "").strip()
247
+ lines.append(f"- **{name}**: {desc}")
248
+ return "\n".join(lines)
249
+ return super()._format_skills_list(skills)
250
+
251
+ def _is_skill_allowed(self, skill_name: str) -> bool:
252
+ if self._allowed_skills is None:
253
+ return True
254
+ for pattern in self._allowed_skills:
255
+ if fnmatch.fnmatch(skill_name, pattern) or fnmatch.fnmatch(skill_name.lower(), pattern.lower()):
256
+ return True
257
+ if ":" in pattern:
258
+ prefix, base_pat = pattern.split(":", 1)
259
+ if base_pat == "*" or fnmatch.fnmatch(skill_name, base_pat) or fnmatch.fnmatch(skill_name.lower(), base_pat.lower()):
260
+ return True
261
+ return False
262
+
263
+ def _get_live_skills(self) -> tuple[list[sdk_skills.SkillMetadata], list[str]]:
264
+ if self._dynamic_sources:
265
+ from opscloud.skills.sources import get_skill_sources
266
+
267
+ live_sources = get_skill_sources(
268
+ project_root=self._project_root,
269
+ include_subagent_skills=self._include_subagent_skills,
270
+ subagents=self._subagents,
271
+ )
272
+ self.sources = [s[0] for s in live_sources]
273
+ self.source_labels = [s[1] for s in live_sources]
274
+ self._namespaces = tuple(s[2] if len(s) == _PLUGIN_SKILL_SOURCE_LENGTH else None for s in live_sources)
275
+
276
+ backend = self._backend
277
+ all_skills: dict[str, sdk_skills.SkillMetadata] = {}
278
+ errors: list[str] = []
279
+
280
+ active_sources: list[tuple[str, str, str | None]] = []
281
+ for source_path, source_label, namespace in zip(
282
+ self.sources, self.source_labels, self._namespaces, strict=True
283
+ ):
284
+ if namespace is None:
285
+ source_skills, source_error = sdk_skills._list_skills_with_errors(backend, source_path)
286
+ if source_error is not None:
287
+ errors.append(source_error)
288
+ else:
289
+ source_skills = load_namespaced_skills(backend, source_path, namespace)
290
+
291
+ source_has_skills = False
292
+ for skill in source_skills:
293
+ skill_name = skill["name"]
294
+ if self._is_skill_allowed(skill_name):
295
+ all_skills[skill_name] = skill
296
+ source_has_skills = True
297
+
298
+ if source_has_skills:
299
+ active_sources.append((source_path, source_label, namespace))
300
+
301
+ if active_sources:
302
+ self.sources = [s[0] for s in active_sources]
303
+ self.source_labels = [s[1] for s in active_sources]
304
+ self._namespaces = tuple(s[2] for s in active_sources)
305
+
306
+ return list(all_skills.values()), errors
307
+
308
+ def before_agent(
309
+ self,
310
+ state: sdk_skills.SkillsState,
311
+ runtime: Runtime,
312
+ config: RunnableConfig,
313
+ ) -> sdk_skills.SkillsStateUpdate | None:
314
+ """Discover live skills and prepare state updates before agent execution.
315
+
316
+ Args:
317
+ state: Current agent state.
318
+ runtime: Active execution runtime.
319
+ config: Runnable configuration for the invocation.
320
+
321
+ Returns:
322
+ SkillsStateUpdate if changes were detected, or None.
323
+ """
324
+ live_skills, errors = self._get_live_skills()
325
+ current_skills = state.get("skills_metadata") or []
326
+ current_names = [s["name"] for s in current_skills]
327
+ live_names = [s["name"] for s in live_skills]
328
+
329
+ if current_names == live_names and state.get("skills_metadata") is not None:
330
+ return None
331
+
332
+ update = sdk_skills.SkillsStateUpdate(skills_metadata=live_skills)
333
+ if errors:
334
+ logger.warning("Skills load errors: %s", errors)
335
+ update["skills_load_errors"] = errors
336
+ return update
337
+
338
+ async def abefore_agent(
339
+ self,
340
+ state: sdk_skills.SkillsState,
341
+ runtime: Runtime,
342
+ config: RunnableConfig,
343
+ ) -> sdk_skills.SkillsStateUpdate | None:
344
+ """Asynchronously discover live skills and prepare state updates before agent execution.
345
+
346
+ Args:
347
+ state: Current agent state.
348
+ runtime: Active execution runtime.
349
+ config: Runnable configuration for the invocation.
350
+
351
+ Returns:
352
+ SkillsStateUpdate if changes were detected, or None.
353
+ """
354
+ return await asyncio.to_thread(self.before_agent, state, runtime, config)
355
+
356
+ def modify_request(self, request: Any) -> Any:
357
+ """Filter out stale skill metadata before formatting the system prompt.
358
+
359
+ Args:
360
+ request: Model request to modify.
361
+
362
+ Returns:
363
+ Modified request with pruned skill metadata.
364
+ """
365
+ live_skills, _ = self._get_live_skills()
366
+ live_names = {s["name"] for s in live_skills}
367
+
368
+ if hasattr(request, "state") and isinstance(request.state, dict):
369
+ # Prune any stale or uninstalled skills from request state before formatting system prompt
370
+ existing = request.state.get("skills_metadata") or []
371
+ request.state["skills_metadata"] = [s for s in existing if s.get("name") in live_names]
372
+ if not request.state["skills_metadata"]:
373
+ request.state["skills_metadata"] = live_skills
374
+
375
+ return super().modify_request(request)
376
+
377
+
378
+ __all__ = [
379
+ "PluginSkillsMiddleware",
380
+ "discover_skill_dirs",
381
+ "load_namespaced_skills",
382
+ ]
@@ -0,0 +1,84 @@
1
+ """Middleware to inject artifact and reporting guidelines for subagents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Any
7
+
8
+ from langchain.agents.middleware.types import (
9
+ AgentMiddleware,
10
+ ExtendedModelResponse,
11
+ ModelRequest,
12
+ ModelResponse,
13
+ )
14
+ from langchain_core.messages import SystemMessage
15
+
16
+ from opscloud.middleware.registry import register_middleware
17
+ from opscloud.utils.logger import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+ ARTIFACT_GUIDELINES = """
22
+ ## Artifacts & Reporting Mechanics
23
+
24
+ You are equipped with the ability to generate "Artifacts". An Artifact is a persistent, well-formatted Markdown document used to present structured information (e.g., an AWS security audit, an architecture review, or tabular pricing data).
25
+
26
+ CRITICAL RULE:
27
+ - Do NOT create excessive markdown summary files after completing work.
28
+ - Focus on the work itself, not documenting what you did.
29
+ - Only create documentation or reports when EXPLICITLY requested by the user. For all other work, use a standard text response.
30
+
31
+ When generating a requested report:
32
+ 1. Use your file writing tools to save the report to the local file system.
33
+ 2. Always save reports in the workspace with a descriptive filename (e.g., `s3_audit_report.md`).
34
+ 3. Format the content using standard GitHub Flavored Markdown (Mermaid diagrams, tables).
35
+ 4. DO NOT re-summarize the entire report in your text response. Simply provide the file path and highlight the top 2 critical findings.
36
+
37
+ ## Context Management & Summarization
38
+ - You are equipped with automatic context compaction and the `compact_conversation` tool.
39
+ - If your delegated task requires extensive exploration, executing many CLI/API commands, or reading large files, older turns are automatically summarized into concise milestones and offloaded to `/conversation_history/` on the workspace backend to preserve your context window.
40
+ - When delivering your final result back to the orchestrator, provide a crisp, direct summary of your actions, discovered findings, and modified resources so the parent agent has immediate clarity.
41
+ """
42
+
43
+ @register_middleware(name="subagent_artifacts")
44
+ class SubagentArtifactsMiddleware(AgentMiddleware[Any, Any]):
45
+ """Middleware that injects Artifact reporting rules into the subagent's system prompt."""
46
+
47
+ def wrap_model_call(
48
+ self,
49
+ request: ModelRequest,
50
+ handler: Callable[[ModelRequest], ModelResponse],
51
+ ) -> ModelResponse | ExtendedModelResponse:
52
+
53
+ # Inject the guidelines into the existing system message
54
+ if request.system_message is not None:
55
+ curr_content = request.system_message.content or ""
56
+ if isinstance(curr_content, str):
57
+ new_content = curr_content + "\n" + ARTIFACT_GUIDELINES
58
+ elif isinstance(curr_content, list):
59
+ new_content = list(curr_content) + [ARTIFACT_GUIDELINES]
60
+ else:
61
+ new_content = ARTIFACT_GUIDELINES
62
+
63
+ request = request.override(system_message=SystemMessage(content=new_content))
64
+
65
+ return handler(request)
66
+
67
+ async def awrap_model_call(
68
+ self,
69
+ request: ModelRequest,
70
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
71
+ ) -> ModelResponse | ExtendedModelResponse:
72
+
73
+ if request.system_message is not None:
74
+ curr_content = request.system_message.content or ""
75
+ if isinstance(curr_content, str):
76
+ new_content = curr_content + "\n" + ARTIFACT_GUIDELINES
77
+ elif isinstance(curr_content, list):
78
+ new_content = list(curr_content) + [ARTIFACT_GUIDELINES]
79
+ else:
80
+ new_content = ARTIFACT_GUIDELINES
81
+
82
+ request = request.override(system_message=SystemMessage(content=new_content))
83
+
84
+ return await handler(request)