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,106 @@
1
+ """OpsCloud middleware modules and centralized registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from opscloud.middleware.registry import (
6
+ BaseAgentMiddleware,
7
+ MiddlewareRegistry,
8
+ get_middleware_registry,
9
+ register_middleware,
10
+ )
11
+ from opscloud.middleware.configurable_model import ConfigurableModelMiddleware
12
+ from opscloud.middleware.jev_model_router import JevDynamicModelRouterMiddleware
13
+ from opscloud.middleware.local_context import LocalContextMiddleware
14
+ from opscloud.middleware.ask_user import AskUserMiddleware
15
+ from opscloud.middleware.resume_state import ResumeStateMiddleware
16
+ from opscloud.middleware.compaction import create_cli_compaction_middleware
17
+ from opscloud.middleware.skills import PluginSkillsMiddleware
18
+ from opscloud.middleware.cost_tracking import ACTIVE_SESSION_THREAD_ID, CostState, CostTrackingMiddleware
19
+ from opscloud.middleware.auto_mode_hitl import AutoModeHITLMiddleware, AutoModeState
20
+ from opscloud.middleware.auto_mode import AUTO_MODE_EVENT_TYPE, is_potentially_destructive
21
+ from opscloud.middleware.subagents import SubagentsMiddleware
22
+ from opscloud.middleware.subagent_telemetry import SubagentTelemetryMiddleware
23
+ from opscloud.middleware.goal_state_notice import (
24
+ GOAL_CONTROL_MESSAGE_SOURCE,
25
+ GOAL_STATE_MESSAGE_SOURCE,
26
+ build_goal_state_notice,
27
+ is_conversation_control_message,
28
+ )
29
+ from opscloud.middleware.goal_tools import GoalToolsMiddleware
30
+ from opscloud.middleware.goal_criteria import (
31
+ GoalAmendRequest,
32
+ GoalCreateRequest,
33
+ GoalCriteriaMiddleware,
34
+ GoalCriteriaRequest,
35
+ GoalCriteriaState,
36
+ GoalProposal,
37
+ )
38
+ from opscloud.middleware.reliable_rubric import ReliableRubricMiddleware
39
+ from opscloud.middleware.unified_system_message import (
40
+ UnifiedSystemMessageMiddleware,
41
+ unify_system_message,
42
+ )
43
+ from opscloud.middleware._repository_bounds import (
44
+ RepositoryBounds,
45
+ REPOSITORY_TOOL_NAMES,
46
+ )
47
+ from opscloud.middleware.glm_stall_recovery import GlmTerminalStallRecoveryMiddleware
48
+ from opscloud.middleware.headless_mcp_guard import HeadlessMCPGuardMiddleware
49
+ from opscloud.middleware.mcp_context import MCPContextMiddleware
50
+ from opscloud.middleware.mcp_middleware import MCPToolMiddleware, normalize_mcp_arguments
51
+ from opscloud.middleware.memory_guard import ManagedMemoryGuardMiddleware
52
+ from opscloud.middleware.model_retry import CodeModelRetryMiddleware
53
+ from opscloud.middleware.server_hooks import ServerHooksMiddleware, ServerHooksState
54
+ from opscloud.middleware.shell_allow_list import ShellAllowListMiddleware
55
+ from opscloud.middleware.tool_filter import ToolFilterMiddleware, TOOL_ALIAS_MAP
56
+
57
+ __all__ = [
58
+ "BaseAgentMiddleware",
59
+ "MiddlewareRegistry",
60
+ "register_middleware",
61
+ "get_middleware_registry",
62
+ "ConfigurableModelMiddleware",
63
+ "JevDynamicModelRouterMiddleware",
64
+ "LocalContextMiddleware",
65
+ "AskUserMiddleware",
66
+ "ResumeStateMiddleware",
67
+ "CostTrackingMiddleware",
68
+ "CostState",
69
+ "ACTIVE_SESSION_THREAD_ID",
70
+ "create_cli_compaction_middleware",
71
+ "PluginSkillsMiddleware",
72
+ "AutoModeHITLMiddleware",
73
+ "AutoModeState",
74
+ "AUTO_MODE_EVENT_TYPE",
75
+ "is_potentially_destructive",
76
+ "SubagentsMiddleware",
77
+ "SubagentTelemetryMiddleware",
78
+ "GoalToolsMiddleware",
79
+ "GoalCriteriaMiddleware",
80
+ "GoalCriteriaRequest",
81
+ "GoalCriteriaState",
82
+ "GoalCreateRequest",
83
+ "GoalAmendRequest",
84
+ "GoalProposal",
85
+ "ReliableRubricMiddleware",
86
+ "UnifiedSystemMessageMiddleware",
87
+ "unify_system_message",
88
+ "GOAL_CONTROL_MESSAGE_SOURCE",
89
+ "GOAL_STATE_MESSAGE_SOURCE",
90
+ "build_goal_state_notice",
91
+ "is_conversation_control_message",
92
+ "RepositoryBounds",
93
+ "REPOSITORY_TOOL_NAMES",
94
+ "GlmTerminalStallRecoveryMiddleware",
95
+ "HeadlessMCPGuardMiddleware",
96
+ "MCPContextMiddleware",
97
+ "MCPToolMiddleware",
98
+ "normalize_mcp_arguments",
99
+ "ManagedMemoryGuardMiddleware",
100
+ "CodeModelRetryMiddleware",
101
+ "ServerHooksMiddleware",
102
+ "ServerHooksState",
103
+ "ShellAllowListMiddleware",
104
+ "ToolFilterMiddleware",
105
+ "TOOL_ALIAS_MAP",
106
+ ]
@@ -0,0 +1,459 @@
1
+ """Shared path-safety and size limits for read-only repository inspection.
2
+
3
+ Both the goal-criteria agent's ``_RepositoryToolBudgetMiddleware`` and the rubric
4
+ grader's read-only tools let an LLM sub-agent inspect working-directory files.
5
+ They must apply identical guarantees: reads stay confined to the repository
6
+ root, symlink escapes are rejected in sandboxes and local filesystems, and every
7
+ result is size bounded so a single tool call cannot blow the sub-agent's context
8
+ budget.
9
+
10
+ ``RepositoryBounds`` centralizes that logic so the middleware and the grader tool
11
+ wrappers share one implementation.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import ast
17
+ import asyncio
18
+ import base64
19
+ import json
20
+ from pathlib import Path, PurePosixPath
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ from deepagents.backends.filesystem import FilesystemBackend
24
+ from deepagents.backends.protocol import SandboxBackendProtocol
25
+
26
+ if TYPE_CHECKING:
27
+ from collections.abc import Sequence
28
+
29
+ from deepagents.backends.protocol import BackendProtocol, FileInfo
30
+
31
+ from opscloud.utils.logger import get_logger
32
+
33
+ logger = get_logger(__name__)
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Budget constants — shared across criteria agents and rubric graders
37
+ # ---------------------------------------------------------------------------
38
+
39
+ REPOSITORY_TOOL_CALL_LIMIT = 25
40
+ """Maximum number of repository-inspection tool calls per sub-agent run."""
41
+
42
+ REPOSITORY_READ_LINE_LIMIT = 120
43
+ """Maximum lines a ``read_file`` call returns before truncation."""
44
+
45
+ REPOSITORY_READ_BYTE_LIMIT = 256_000
46
+ """Maximum file size (bytes) that preflight allows for a ``read_file`` call."""
47
+
48
+ REPOSITORY_DIRECTORY_ENTRY_LIMIT = 200
49
+ """Maximum directory entries preflight allows for an ``ls`` call."""
50
+
51
+ REPOSITORY_GLOB_MATCH_LIMIT = 200
52
+ """Maximum match count for a ``glob`` result before truncation."""
53
+
54
+ REPOSITORY_GREP_MATCH_LIMIT = 100
55
+ """Maximum match count for a ``grep`` result before truncation."""
56
+
57
+ REPOSITORY_TOOL_RESULT_LIMIT = 12_000
58
+ """Character limit for any single repository tool result body."""
59
+
60
+ REPOSITORY_TOOL_NAMES = frozenset({"ls", "read_file", "glob", "grep"})
61
+ """Tool names that are valid for read-only repository inspection."""
62
+
63
+ REPOSITORY_PATH_RESULT_PREFIX = "__OPSCLOUD_REPOSITORY_PATH__"
64
+ """Marker emitted by sandbox containment probes."""
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Error messages
68
+ # ---------------------------------------------------------------------------
69
+
70
+ REPOSITORY_PATH_ERROR = "Repository path is unavailable."
71
+ REPOSITORY_UNAVAILABLE_ERROR = "Repository is temporarily unavailable; the path could not be verified."
72
+ REPOSITORY_SIZE_ERROR = "Repository file exceeds the size limit."
73
+ REPOSITORY_LISTING_ERROR = "Repository directory exceeds the listing limit."
74
+ REPOSITORY_READ_ONLY_ERROR = "Repository inspection is limited to read-only tools."
75
+
76
+ _BACKEND_ERRORS: tuple[type[BaseException], ...] = (
77
+ NotImplementedError,
78
+ OSError,
79
+ RuntimeError,
80
+ TypeError,
81
+ ValueError,
82
+ )
83
+
84
+
85
+ class RepositoryBounds:
86
+ """Path-safety and size limits for read-only repository inspection tools."""
87
+
88
+ def __init__(
89
+ self,
90
+ backend: BackendProtocol,
91
+ *,
92
+ root: str = "/",
93
+ allowed_tools: Sequence[str] | None = None,
94
+ ) -> None:
95
+ """Initialize RepositoryBounds with backend and access boundaries.
96
+
97
+ Args:
98
+ backend: Storage or execution backend to guard.
99
+ root: Root path constraint for repository inspection.
100
+ allowed_tools: Sequence of tool names permitted for execution.
101
+ """
102
+ normalized = root.replace("\\", "/")
103
+ path = PurePosixPath(normalized)
104
+ if not normalized.startswith("/") or ".." in path.parts or "~" in root:
105
+ msg = f"Repository root must be an absolute contained path: {root!r}"
106
+ raise ValueError(msg)
107
+ self._backend = backend
108
+ self._root = str(path)
109
+ self._allowed_tools = frozenset(allowed_tools) if allowed_tools is not None else REPOSITORY_TOOL_NAMES
110
+ self._sandbox = backend if isinstance(backend, SandboxBackendProtocol) else None
111
+ self._filesystem = backend if self._sandbox is None and isinstance(backend, FilesystemBackend) else None
112
+ self._filesystem_root: Path | None = None
113
+ if self._filesystem is not None:
114
+ try:
115
+ self._filesystem_root = self._resolve_filesystem_path(self._root)
116
+ except _BACKEND_ERRORS:
117
+ logger.warning(
118
+ "Could not resolve the local repository root; local repository paths will be unavailable",
119
+ exc_info=True,
120
+ )
121
+
122
+ @property
123
+ def root(self) -> str:
124
+ """Absolute path that bounds repository reads."""
125
+ return self._root
126
+
127
+ def safe_path(self, raw_path: str) -> bool:
128
+ """Return whether an explicit repository path is absolute and contained."""
129
+ path = PurePosixPath(raw_path.replace("\\", "/"))
130
+ root = PurePosixPath(self._root)
131
+ return (
132
+ raw_path.startswith("/")
133
+ and ".." not in path.parts
134
+ and "~" not in raw_path
135
+ and (root == PurePosixPath("/") or path == root or root in path.parents)
136
+ )
137
+
138
+ @staticmethod
139
+ def safe_pattern(pattern: str) -> bool:
140
+ """Return whether a relative or absolute glob pattern cannot traverse."""
141
+ path = PurePosixPath(pattern.replace("\\", "/"))
142
+ return ".." not in path.parts and "~" not in pattern
143
+
144
+ def _resolve_filesystem_path(self, raw_path: str) -> Path:
145
+ if self._filesystem is None:
146
+ msg = "Local filesystem backend is unavailable."
147
+ raise RuntimeError(msg)
148
+ if self._filesystem.virtual_mode:
149
+ return (self._filesystem.cwd / raw_path.lstrip("/")).resolve(strict=False)
150
+ return Path(raw_path).resolve(strict=False)
151
+
152
+ def _filesystem_contains(self, raw_path: str) -> bool:
153
+ if self._filesystem is None:
154
+ return True
155
+ if self._filesystem_root is None:
156
+ return False
157
+ try:
158
+ resolved = self._resolve_filesystem_path(raw_path)
159
+ except _BACKEND_ERRORS:
160
+ logger.warning(
161
+ "Local repository containment check failed; treating the path as unavailable",
162
+ exc_info=True,
163
+ )
164
+ return False
165
+ return resolved == self._filesystem_root or self._filesystem_root in resolved.parents
166
+
167
+ def _containment_command(self, raw_path: str) -> str:
168
+ payload = base64.b64encode(json.dumps([self._root, raw_path]).encode()).decode()
169
+ return (
170
+ 'python3 -c "import base64,json,os;'
171
+ f"values=json.loads(base64.b64decode('{payload}'));"
172
+ "root=os.path.realpath(values[0]);path=os.path.realpath(values[1]);"
173
+ "contained=os.path.commonpath([root,path])==root;"
174
+ f"print('{REPOSITORY_PATH_RESULT_PREFIX}'+str(int(contained)))\""
175
+ )
176
+
177
+ def sandbox_contains(self, raw_path: str) -> bool:
178
+ """Check synchronously whether a target path is contained within the root boundary.
179
+
180
+ Args:
181
+ raw_path: Filesystem path to test.
182
+
183
+ Returns:
184
+ True if contained within the root, False otherwise.
185
+ """
186
+ if self._sandbox is None:
187
+ return self._filesystem_contains(raw_path)
188
+ try:
189
+ result = self._sandbox.execute(self._containment_command(raw_path))
190
+ except _BACKEND_ERRORS:
191
+ logger.warning(
192
+ "Repository containment check failed; treating the path as unavailable",
193
+ exc_info=True,
194
+ )
195
+ return False
196
+ return result.exit_code in {None, 0} and any(
197
+ line == f"{REPOSITORY_PATH_RESULT_PREFIX}1" for line in result.output.splitlines()
198
+ )
199
+
200
+ async def asandbox_contains(self, raw_path: str) -> bool:
201
+ """Check asynchronously whether a target path is contained within the root boundary.
202
+
203
+ Args:
204
+ raw_path: Filesystem path to test.
205
+
206
+ Returns:
207
+ True if contained within the root, False otherwise.
208
+ """
209
+ if self._sandbox is None:
210
+ if self._filesystem is None:
211
+ return True
212
+ return await asyncio.to_thread(self._filesystem_contains, raw_path)
213
+ try:
214
+ result = await self._sandbox.aexecute(self._containment_command(raw_path))
215
+ except _BACKEND_ERRORS:
216
+ logger.warning(
217
+ "Repository containment check failed; treating the path as unavailable",
218
+ exc_info=True,
219
+ )
220
+ return False
221
+ return result.exit_code in {None, 0} and any(
222
+ line == f"{REPOSITORY_PATH_RESULT_PREFIX}1" for line in result.output.splitlines()
223
+ )
224
+
225
+ @staticmethod
226
+ def entry_size(
227
+ entries: Sequence[FileInfo] | None,
228
+ normalized_path: str,
229
+ ) -> int | None:
230
+ """Look up file size for a normalized path in directory listing entries.
231
+
232
+ Args:
233
+ entries: Sequence of FileInfo entries.
234
+ normalized_path: Normalized POSIX path string.
235
+
236
+ Returns:
237
+ File size in bytes if found, or None.
238
+ """
239
+ for item in entries or []:
240
+ raw = item.get("path") if isinstance(item, dict) else None
241
+ if not isinstance(raw, str):
242
+ continue
243
+ if str(PurePosixPath(raw)) == normalized_path:
244
+ size = item.get("size")
245
+ return size if isinstance(size, int) else None
246
+ return None
247
+
248
+ def _validate_search_paths(
249
+ self,
250
+ name: str,
251
+ args: dict[str, Any],
252
+ ) -> str | None:
253
+ path = args.get("path")
254
+ if path is not None and (not isinstance(path, str) or not self.safe_path(path)):
255
+ return REPOSITORY_PATH_ERROR
256
+
257
+ patterns = [args.get("pattern")] if name == "glob" else [args.get("glob")]
258
+ if any(
259
+ pattern is not None and (not isinstance(pattern, str) or not self.safe_pattern(pattern))
260
+ for pattern in patterns
261
+ ):
262
+ return REPOSITORY_PATH_ERROR
263
+ return None
264
+
265
+ def preflight(self, name: str, args: dict[str, Any]) -> str | None:
266
+ """Perform synchronous safety, path traversal, and size preflight checks.
267
+
268
+ Args:
269
+ name: Tool name being invoked.
270
+ args: Tool execution argument dictionary.
271
+
272
+ Returns:
273
+ Error message string if validation failed, or None if passed.
274
+ """
275
+ if name not in self._allowed_tools:
276
+ return REPOSITORY_READ_ONLY_ERROR
277
+ if name == "execute":
278
+ return None
279
+ if name in {"glob", "grep"}:
280
+ error = self._validate_search_paths(name, args)
281
+ if error is not None:
282
+ return error
283
+ raw_path = args.get("path")
284
+ if raw_path is None:
285
+ raw_path = self._root
286
+ if not isinstance(raw_path, str) or not self.sandbox_contains(raw_path):
287
+ return REPOSITORY_PATH_ERROR
288
+ return None
289
+
290
+ key = "file_path" if name == "read_file" else "path"
291
+ raw_path = args.get(key)
292
+ if not isinstance(raw_path, str):
293
+ return None
294
+
295
+ path = PurePosixPath(raw_path.replace("\\", "/"))
296
+ if not self.safe_path(raw_path):
297
+ return REPOSITORY_PATH_ERROR
298
+ if not self.sandbox_contains(raw_path):
299
+ return REPOSITORY_PATH_ERROR
300
+
301
+ try:
302
+ result = self._backend.ls(raw_path if name == "ls" else str(path.parent))
303
+ except _BACKEND_ERRORS:
304
+ logger.warning(
305
+ "Repository preflight failed for tool %r; treating the repository as temporarily unavailable",
306
+ name,
307
+ exc_info=True,
308
+ )
309
+ return REPOSITORY_UNAVAILABLE_ERROR
310
+ if result.error is not None:
311
+ return REPOSITORY_PATH_ERROR
312
+ if name == "ls":
313
+ if len(result.entries or []) > REPOSITORY_DIRECTORY_ENTRY_LIMIT:
314
+ return REPOSITORY_LISTING_ERROR
315
+ else: # read_file
316
+ size = self.entry_size(result.entries, str(path))
317
+ if size is not None and size > REPOSITORY_READ_BYTE_LIMIT:
318
+ return REPOSITORY_SIZE_ERROR
319
+ return None
320
+
321
+ async def apreflight(self, name: str, args: dict[str, Any]) -> str | None:
322
+ """Perform asynchronous safety, path traversal, and size preflight checks.
323
+
324
+ Args:
325
+ name: Tool name being invoked.
326
+ args: Tool execution argument dictionary.
327
+
328
+ Returns:
329
+ Error message string if validation failed, or None if passed.
330
+ """
331
+ if name not in self._allowed_tools:
332
+ return REPOSITORY_READ_ONLY_ERROR
333
+ if name == "execute":
334
+ return None
335
+ if name in {"glob", "grep"}:
336
+ error = self._validate_search_paths(name, args)
337
+ if error is not None:
338
+ return error
339
+ raw_path = args.get("path")
340
+ if raw_path is None:
341
+ raw_path = self._root
342
+ if not isinstance(raw_path, str) or not await self.asandbox_contains(raw_path):
343
+ return REPOSITORY_PATH_ERROR
344
+ return None
345
+
346
+ key = "file_path" if name == "read_file" else "path"
347
+ raw_path = args.get(key)
348
+ if not isinstance(raw_path, str):
349
+ return None
350
+
351
+ path = PurePosixPath(raw_path.replace("\\", "/"))
352
+ if not self.safe_path(raw_path):
353
+ return REPOSITORY_PATH_ERROR
354
+ if not await self.asandbox_contains(raw_path):
355
+ return REPOSITORY_PATH_ERROR
356
+
357
+ try:
358
+ result = await self._backend.als(raw_path if name == "ls" else str(path.parent))
359
+ except _BACKEND_ERRORS:
360
+ logger.warning(
361
+ "Repository preflight failed for tool %r; treating the repository as temporarily unavailable",
362
+ name,
363
+ exc_info=True,
364
+ )
365
+ return REPOSITORY_UNAVAILABLE_ERROR
366
+ if result.error is not None:
367
+ return REPOSITORY_PATH_ERROR
368
+ if name == "ls":
369
+ if len(result.entries or []) > REPOSITORY_DIRECTORY_ENTRY_LIMIT:
370
+ return REPOSITORY_LISTING_ERROR
371
+ elif name == "read_file":
372
+ size = self.entry_size(result.entries, str(path))
373
+ if size is not None and size > REPOSITORY_READ_BYTE_LIMIT:
374
+ return REPOSITORY_SIZE_ERROR
375
+ return None
376
+
377
+ def clamp_args(self, name: str, args: dict[str, Any]) -> dict[str, Any]:
378
+ """Clamp tool arguments to safe limits (line limits, match count, root path).
379
+
380
+ Args:
381
+ name: Tool name being invoked.
382
+ args: Raw tool arguments.
383
+
384
+ Returns:
385
+ New argument dictionary with clamped values.
386
+ """
387
+ clamped = dict(args)
388
+ if name == "read_file":
389
+ limit = clamped.get("limit", REPOSITORY_READ_LINE_LIMIT)
390
+ if not isinstance(limit, int) or isinstance(limit, bool):
391
+ limit = REPOSITORY_READ_LINE_LIMIT
392
+ clamped["limit"] = max(1, min(limit, REPOSITORY_READ_LINE_LIMIT))
393
+ elif name in {"glob", "grep"} and clamped.get("path") is None:
394
+ clamped["path"] = self._root
395
+ if name == "grep":
396
+ count = clamped.get("max_count", REPOSITORY_GREP_MATCH_LIMIT)
397
+ if not isinstance(count, int) or isinstance(count, bool) or count <= 0:
398
+ count = REPOSITORY_GREP_MATCH_LIMIT
399
+ clamped["max_count"] = min(count, REPOSITORY_GREP_MATCH_LIMIT)
400
+ return clamped
401
+
402
+ @staticmethod
403
+ def bounded_glob_content(content: str) -> str:
404
+ """Truncate glob output list to the maximum match limit.
405
+
406
+ Args:
407
+ content: Raw tool output string containing python list literal.
408
+
409
+ Returns:
410
+ Truncated string representation if over the limit, or original content.
411
+ """
412
+ body, separator, notes = content.partition("\n\n")
413
+ try:
414
+ paths = ast.literal_eval(body)
415
+ except (SyntaxError, ValueError):
416
+ return content
417
+ if not isinstance(paths, list) or not all(isinstance(path, str) for path in paths):
418
+ return content
419
+ if len(paths) <= REPOSITORY_GLOB_MATCH_LIMIT:
420
+ return content
421
+ marker = f"[Glob results limited to the first {REPOSITORY_GLOB_MATCH_LIMIT} matches.]"
422
+ bounded = str(paths[:REPOSITORY_GLOB_MATCH_LIMIT])
423
+ suffix = f"\n\n{notes}" if separator and notes else ""
424
+ return f"{bounded}\n\n{marker}{suffix}"
425
+
426
+ def bound_text(self, name: str, content: str) -> str:
427
+ """Enforce maximum byte length limits on tool result output.
428
+
429
+ Args:
430
+ name: Tool name invoked.
431
+ content: Raw tool output text.
432
+
433
+ Returns:
434
+ Bounded text with truncation indicator if necessary.
435
+ """
436
+ if name == "glob":
437
+ content = self.bounded_glob_content(content)
438
+ if len(content) > REPOSITORY_TOOL_RESULT_LIMIT:
439
+ marker = "\n[Repository tool result shortened to the context limit.]"
440
+ content = content[: REPOSITORY_TOOL_RESULT_LIMIT - len(marker)] + marker
441
+ return content
442
+
443
+
444
+ __all__ = [
445
+ "REPOSITORY_DIRECTORY_ENTRY_LIMIT",
446
+ "REPOSITORY_GLOB_MATCH_LIMIT",
447
+ "REPOSITORY_GREP_MATCH_LIMIT",
448
+ "REPOSITORY_LISTING_ERROR",
449
+ "REPOSITORY_PATH_ERROR",
450
+ "REPOSITORY_READ_BYTE_LIMIT",
451
+ "REPOSITORY_READ_LINE_LIMIT",
452
+ "REPOSITORY_READ_ONLY_ERROR",
453
+ "REPOSITORY_SIZE_ERROR",
454
+ "REPOSITORY_TOOL_CALL_LIMIT",
455
+ "REPOSITORY_TOOL_NAMES",
456
+ "REPOSITORY_TOOL_RESULT_LIMIT",
457
+ "REPOSITORY_UNAVAILABLE_ERROR",
458
+ "RepositoryBounds",
459
+ ]