talkops-opscloud 0.3.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opscloud/__init__.py +5 -0
- opscloud/__main__.py +6 -0
- opscloud/_constants.py +53 -0
- opscloud/_debug.py +145 -0
- opscloud/_debug_buffer.py +250 -0
- opscloud/_textual_patches.py +367 -0
- opscloud/_version.py +15 -0
- opscloud/agent/__init__.py +27 -0
- opscloud/agent/config.py +168 -0
- opscloud/agent/factory.py +1103 -0
- opscloud/approval_mode.py +44 -0
- opscloud/backend/__init__.py +49 -0
- opscloud/backend/composite.py +89 -0
- opscloud/backend/local.py +183 -0
- opscloud/backend/registry.py +79 -0
- opscloud/backend/sandbox_config.py +107 -0
- opscloud/backend/sandbox_factory.py +432 -0
- opscloud/built_in_skills/remember/SKILL.md +124 -0
- opscloud/built_in_skills/skill-creator/SKILL.md +348 -0
- opscloud/built_in_skills/skill-creator/scripts/init_skill.py +371 -0
- opscloud/built_in_skills/skill-creator/scripts/quick_validate.py +165 -0
- opscloud/cli/__init__.py +1 -0
- opscloud/cli/commands/__init__.py +15 -0
- opscloud/cli/commands/auth.py +45 -0
- opscloud/cli/commands/doctor.py +100 -0
- opscloud/cli/commands/mcp.py +137 -0
- opscloud/cli/commands/threads.py +51 -0
- opscloud/cli/interactive.py +154 -0
- opscloud/cli/main.py +903 -0
- opscloud/cli/non_interactive.py +425 -0
- opscloud/cli/server_manager.py +58 -0
- opscloud/commands/__init__.py +19 -0
- opscloud/commands/_base.py +110 -0
- opscloud/commands/_guards.py +52 -0
- opscloud/commands/_router.py +146 -0
- opscloud/commands/_types.py +36 -0
- opscloud/commands/core/__init__.py +47 -0
- opscloud/commands/core/auth.py +94 -0
- opscloud/commands/core/bug.py +46 -0
- opscloud/commands/core/clear.py +106 -0
- opscloud/commands/core/cloud.py +185 -0
- opscloud/commands/core/compact.py +128 -0
- opscloud/commands/core/config_cmd.py +120 -0
- opscloud/commands/core/context.py +220 -0
- opscloud/commands/core/cost.py +44 -0
- opscloud/commands/core/doctor.py +535 -0
- opscloud/commands/core/effort.py +108 -0
- opscloud/commands/core/exit_cmd.py +41 -0
- opscloud/commands/core/fast.py +148 -0
- opscloud/commands/core/help_cmd.py +94 -0
- opscloud/commands/core/mcp.py +134 -0
- opscloud/commands/core/model.py +109 -0
- opscloud/commands/core/permissions.py +196 -0
- opscloud/commands/core/plugins.py +92 -0
- opscloud/commands/core/pool.py +108 -0
- opscloud/commands/core/resume.py +97 -0
- opscloud/commands/core/skills.py +178 -0
- opscloud/commands/power/__init__.py +51 -0
- opscloud/commands/power/agents.py +98 -0
- opscloud/commands/power/btw.py +85 -0
- opscloud/commands/power/copy.py +97 -0
- opscloud/commands/power/goal.py +548 -0
- opscloud/commands/power/loop.py +295 -0
- opscloud/commands/power/memory.py +287 -0
- opscloud/commands/power/review.py +152 -0
- opscloud/commands/power/rubric.py +287 -0
- opscloud/commands/power/runtime.py +321 -0
- opscloud/commands/power/skill_creator.py +70 -0
- opscloud/commands/power/skill_invoke.py +165 -0
- opscloud/commands/power/tasks.py +209 -0
- opscloud/commands/power/trace.py +117 -0
- opscloud/commands/power/ui_toggles.py +135 -0
- opscloud/commands/power/version.py +52 -0
- opscloud/commands/registry.py +176 -0
- opscloud/config/__init__.py +114 -0
- opscloud/config/adapters/__init__.py +5 -0
- opscloud/config/adapters/postgres.py +776 -0
- opscloud/config/adapters/sqlite.py +769 -0
- opscloud/config/aws.py +248 -0
- opscloud/config/cloud_profiles.py +437 -0
- opscloud/config/env_vars.py +81 -0
- opscloud/config/langsmith.py +319 -0
- opscloud/config/manifest.py +817 -0
- opscloud/config/metadata.py +205 -0
- opscloud/config/paths.py +604 -0
- opscloud/config/plugins.py +130 -0
- opscloud/config/settings.py +834 -0
- opscloud/config/store.py +453 -0
- opscloud/config/store_factory.py +97 -0
- opscloud/config/toml_config.py +562 -0
- opscloud/editor.py +173 -0
- opscloud/exceptions.py +65 -0
- opscloud/file_ops.py +217 -0
- opscloud/hooks/__init__.py +139 -0
- opscloud/hooks/env.py +44 -0
- opscloud/hooks/interrupt.py +121 -0
- opscloud/hooks/models/__init__.py +109 -0
- opscloud/hooks/models/adapters.py +19 -0
- opscloud/hooks/models/config.py +79 -0
- opscloud/hooks/models/domain.py +434 -0
- opscloud/hooks/models/transport.py +37 -0
- opscloud/hooks/tools.py +231 -0
- opscloud/input.py +48 -0
- opscloud/integrations/__init__.py +80 -0
- opscloud/integrations/base.py +153 -0
- opscloud/integrations/content_filter.py +119 -0
- opscloud/integrations/event_bus.py +480 -0
- opscloud/integrations/hooks.py +238 -0
- opscloud/integrations/identity.py +181 -0
- opscloud/integrations/notifications.py +134 -0
- opscloud/integrations/stream_bridge.py +397 -0
- opscloud/integrations/thread_store.py +81 -0
- opscloud/json_types.py +20 -0
- opscloud/mcp/__init__.py +57 -0
- opscloud/mcp/config.py +203 -0
- opscloud/mcp/discovery.py +229 -0
- opscloud/mcp/mcp_info.py +109 -0
- opscloud/mcp/middleware.py +102 -0
- opscloud/mcp/preload.py +505 -0
- opscloud/mcp/raw_config.py +149 -0
- opscloud/mcp/semantic_profiler.py +305 -0
- opscloud/mcp/session_manager.py +727 -0
- opscloud/mcp/trust.py +105 -0
- opscloud/memory/__init__.py +27 -0
- opscloud/memory/branch.py +61 -0
- opscloud/memory/guard.py +19 -0
- opscloud/memory/onboarding.py +120 -0
- opscloud/memory/registry.py +134 -0
- opscloud/memory/store.py +223 -0
- opscloud/middleware/__init__.py +106 -0
- opscloud/middleware/_repository_bounds.py +459 -0
- opscloud/middleware/ask_user.py +152 -0
- opscloud/middleware/auto_mode.py +60 -0
- opscloud/middleware/auto_mode_hitl.py +2434 -0
- opscloud/middleware/compaction.py +566 -0
- opscloud/middleware/configurable_model.py +233 -0
- opscloud/middleware/cost_tracking.py +1021 -0
- opscloud/middleware/glm_stall_recovery.py +109 -0
- opscloud/middleware/goal_criteria.py +1234 -0
- opscloud/middleware/goal_state_notice.py +712 -0
- opscloud/middleware/goal_tools.py +407 -0
- opscloud/middleware/headless_mcp_guard.py +88 -0
- opscloud/middleware/jev_model_router.py +1018 -0
- opscloud/middleware/local_context.py +460 -0
- opscloud/middleware/mcp_context.py +151 -0
- opscloud/middleware/mcp_middleware.py +146 -0
- opscloud/middleware/memory_guard.py +393 -0
- opscloud/middleware/model_retry.py +205 -0
- opscloud/middleware/registry.py +137 -0
- opscloud/middleware/reliable_rubric.py +709 -0
- opscloud/middleware/resume_state.py +91 -0
- opscloud/middleware/server_hooks.py +1189 -0
- opscloud/middleware/shell_allow_list.py +90 -0
- opscloud/middleware/skills.py +382 -0
- opscloud/middleware/subagent_artifacts.py +84 -0
- opscloud/middleware/subagent_telemetry.py +261 -0
- opscloud/middleware/subagents.py +243 -0
- opscloud/middleware/tool_filter.py +384 -0
- opscloud/middleware/unified_system_message.py +62 -0
- opscloud/model/__init__.py +37 -0
- opscloud/model/config.py +2992 -0
- opscloud/model/factory.py +477 -0
- opscloud/model/pool.py +459 -0
- opscloud/model/reasoning.py +282 -0
- opscloud/offload.py +173 -0
- opscloud/output.py +64 -0
- opscloud/plugins/__init__.py +189 -0
- opscloud/plugins/_json.py +22 -0
- opscloud/plugins/adapters/__init__.py +13 -0
- opscloud/plugins/adapters/agents.py +112 -0
- opscloud/plugins/adapters/commands.py +190 -0
- opscloud/plugins/adapters/mcp.py +363 -0
- opscloud/plugins/adapters/skills.py +132 -0
- opscloud/plugins/commands_cli.py +541 -0
- opscloud/plugins/discovery.py +512 -0
- opscloud/plugins/manifest.py +335 -0
- opscloud/plugins/marketplace.py +893 -0
- opscloud/plugins/models.py +334 -0
- opscloud/plugins/project_plugins.py +317 -0
- opscloud/plugins/protocol.py +31 -0
- opscloud/plugins/store.py +905 -0
- opscloud/plugins/substitution.py +122 -0
- opscloud/project_utils.py +145 -0
- opscloud/prompts/__init__.py +37 -0
- opscloud/prompts/system.py +483 -0
- opscloud/prompts/templates/system_prompt.md +166 -0
- opscloud/prompts/types.py +54 -0
- opscloud/rubrics/__init__.py +42 -0
- opscloud/rubrics/evaluator.py +404 -0
- opscloud/rubrics/evidence_extractor.py +229 -0
- opscloud/rubrics/generator.py +239 -0
- opscloud/rubrics/jev_compiler.py +175 -0
- opscloud/rubrics/jev_grader.py +297 -0
- opscloud/rubrics/middleware.py +8 -0
- opscloud/schema/__init__.py +107 -0
- opscloud/schema/interrupts.py +981 -0
- opscloud/security/__init__.py +91 -0
- opscloud/security/approval_mode.py +528 -0
- opscloud/security/approval_mode_source.py +230 -0
- opscloud/security/cli_ast_evaluator.py +552 -0
- opscloud/security/jev_classifier.py +651 -0
- opscloud/security/shell_safety.py +340 -0
- opscloud/security/unicode_security.py +439 -0
- opscloud/security/url_validation.py +160 -0
- opscloud/server/__init__.py +13 -0
- opscloud/server/_server_config.py +158 -0
- opscloud/server/server.py +242 -0
- opscloud/server/server_graph.py +93 -0
- opscloud/skills/__init__.py +91 -0
- opscloud/skills/commands.py +1099 -0
- opscloud/skills/invocation.py +200 -0
- opscloud/skills/load.py +388 -0
- opscloud/skills/loader.py +23 -0
- opscloud/skills/merge.py +65 -0
- opscloud/skills/registry.py +263 -0
- opscloud/skills/sources.py +174 -0
- opscloud/skills/trust.py +417 -0
- opscloud/state/__init__.py +142 -0
- opscloud/state/base.py +154 -0
- opscloud/state/cloud_context.py +262 -0
- opscloud/state/goal_channels.py +124 -0
- opscloud/state/goal_state_limits.py +175 -0
- opscloud/state/resume_state.py +58 -0
- opscloud/state/service_context.py +281 -0
- opscloud/state/session.py +957 -0
- opscloud/state/state_migration.py +86 -0
- opscloud/subagents/__init__.py +30 -0
- opscloud/subagents/loader.py +331 -0
- opscloud/subagents/subagents_parser.py +117 -0
- opscloud/subagents/types.py +25 -0
- opscloud/tools/__init__.py +36 -0
- opscloud/tools/catalog.py +37 -0
- opscloud/tools/display.py +24 -0
- opscloud/tools/fetch_url.py +176 -0
- opscloud/tools/goal_tools.py +423 -0
- opscloud/tools/registry.py +105 -0
- opscloud/tools/thread.py +32 -0
- opscloud/tools/web_search.py +215 -0
- opscloud/ui/__init__.py +119 -0
- opscloud/ui/app.py +5400 -0
- opscloud/ui/app.tcss +479 -0
- opscloud/ui/clipboard.py +147 -0
- opscloud/ui/command_registry.py +482 -0
- opscloud/ui/message_store.py +152 -0
- opscloud/ui/permission_store.py +360 -0
- opscloud/ui/preferences.py +113 -0
- opscloud/ui/prompt_manager.py +96 -0
- opscloud/ui/remote_client.py +505 -0
- opscloud/ui/textual_adapter.py +1269 -0
- opscloud/ui/theme.py +728 -0
- opscloud/ui/ui_help.py +415 -0
- opscloud/ui/widgets/__init__.py +383 -0
- opscloud/ui/widgets/_ask_user_types.py +81 -0
- opscloud/ui/widgets/_copy_spans.py +52 -0
- opscloud/ui/widgets/_inline_prompt.py +290 -0
- opscloud/ui/widgets/_js_eval_display.py +139 -0
- opscloud/ui/widgets/_links.py +175 -0
- opscloud/ui/widgets/_tool_stream.py +308 -0
- opscloud/ui/widgets/agent_selector.py +297 -0
- opscloud/ui/widgets/approval.py +741 -0
- opscloud/ui/widgets/ask_user.py +539 -0
- opscloud/ui/widgets/auth.py +695 -0
- opscloud/ui/widgets/auth_manager.py +214 -0
- opscloud/ui/widgets/auto_mode_notice.py +146 -0
- opscloud/ui/widgets/autocomplete.py +709 -0
- opscloud/ui/widgets/chat_input.py +805 -0
- opscloud/ui/widgets/cloud_selector.py +528 -0
- opscloud/ui/widgets/config_manager.py +652 -0
- opscloud/ui/widgets/debug_console.py +1269 -0
- opscloud/ui/widgets/devops_renderers.py +184 -0
- opscloud/ui/widgets/diff.py +237 -0
- opscloud/ui/widgets/effort_selector.py +166 -0
- opscloud/ui/widgets/goal_review.py +612 -0
- opscloud/ui/widgets/goal_status.py +57 -0
- opscloud/ui/widgets/infra_panel.py +141 -0
- opscloud/ui/widgets/install_confirm.py +114 -0
- opscloud/ui/widgets/loading.py +170 -0
- opscloud/ui/widgets/mcp_viewer.py +1204 -0
- opscloud/ui/widgets/messages.py +4461 -0
- opscloud/ui/widgets/model_selector.py +863 -0
- opscloud/ui/widgets/notification_center.py +108 -0
- opscloud/ui/widgets/notification_settings.py +119 -0
- opscloud/ui/widgets/operation_card.py +106 -0
- opscloud/ui/widgets/permissions_manager.py +667 -0
- opscloud/ui/widgets/plugin_manager.py +1212 -0
- opscloud/ui/widgets/pool_selector.py +376 -0
- opscloud/ui/widgets/preamble.py +53 -0
- opscloud/ui/widgets/skills_viewer.py +432 -0
- opscloud/ui/widgets/status.py +1037 -0
- opscloud/ui/widgets/subagent_panel.py +927 -0
- opscloud/ui/widgets/theme_selector.py +232 -0
- opscloud/ui/widgets/thread_selector.py +358 -0
- opscloud/ui/widgets/toast.py +31 -0
- opscloud/ui/widgets/tool_display.py +325 -0
- opscloud/ui/widgets/tool_renderers.py +234 -0
- opscloud/ui/widgets/tool_widgets.py +278 -0
- opscloud/ui/widgets/welcome.py +421 -0
- opscloud/ui/widgets/welcome_popup.py +102 -0
- opscloud/utils/__init__.py +24 -0
- opscloud/utils/cost_estimation.py +169 -0
- opscloud/utils/git.py +390 -0
- opscloud/utils/logger.py +571 -0
- opscloud/utils/session_stats.py +115 -0
- opscloud/utils/startup_error.py +37 -0
- talkops_opscloud-0.3.0b1.dist-info/METADATA +449 -0
- talkops_opscloud-0.3.0b1.dist-info/RECORD +309 -0
- talkops_opscloud-0.3.0b1.dist-info/WHEEL +4 -0
- talkops_opscloud-0.3.0b1.dist-info/entry_points.txt +2 -0
- talkops_opscloud-0.3.0b1.dist-info/licenses/LICENSE +175 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Approval modes for OpsCloud.
|
|
2
|
+
|
|
3
|
+
Re-exports canonical definitions and store/acknowledgement utilities from `opscloud.security.approval_mode`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from opscloud.security.approval_mode import (
|
|
7
|
+
APPROVAL_MODE_NAMESPACE,
|
|
8
|
+
AUTO_NOTICE_VERSION,
|
|
9
|
+
ApprovalMode,
|
|
10
|
+
ApprovalModePayload,
|
|
11
|
+
approval_mode_key,
|
|
12
|
+
approval_mode_payload,
|
|
13
|
+
approval_mode_scope,
|
|
14
|
+
approval_state_path,
|
|
15
|
+
aread_approval_mode_from_store,
|
|
16
|
+
awrite_approval_mode,
|
|
17
|
+
coerce_approval_mode,
|
|
18
|
+
get_approval_mode,
|
|
19
|
+
has_auto_mode_notice,
|
|
20
|
+
next_approval_mode,
|
|
21
|
+
read_approval_mode_from_store,
|
|
22
|
+
save_auto_mode_notice,
|
|
23
|
+
set_approval_mode,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"APPROVAL_MODE_NAMESPACE",
|
|
28
|
+
"AUTO_NOTICE_VERSION",
|
|
29
|
+
"ApprovalMode",
|
|
30
|
+
"ApprovalModePayload",
|
|
31
|
+
"approval_mode_key",
|
|
32
|
+
"approval_mode_payload",
|
|
33
|
+
"approval_mode_scope",
|
|
34
|
+
"approval_state_path",
|
|
35
|
+
"aread_approval_mode_from_store",
|
|
36
|
+
"awrite_approval_mode",
|
|
37
|
+
"coerce_approval_mode",
|
|
38
|
+
"get_approval_mode",
|
|
39
|
+
"has_auto_mode_notice",
|
|
40
|
+
"next_approval_mode",
|
|
41
|
+
"read_approval_mode_from_store",
|
|
42
|
+
"save_auto_mode_notice",
|
|
43
|
+
"set_approval_mode",
|
|
44
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Backend module for OpsCloud — shell execution, sandbox environments, and filesystem access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from opscloud.backend.registry import (
|
|
8
|
+
BackendRegistry,
|
|
9
|
+
get_backend_class,
|
|
10
|
+
get_backend_registry,
|
|
11
|
+
register_backend,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BackendRegistry",
|
|
16
|
+
"CompositeBackend",
|
|
17
|
+
"LocalShellBackend",
|
|
18
|
+
"OpsCloudCompositeBackend",
|
|
19
|
+
"SandboxConfig",
|
|
20
|
+
"cleanup_all_sandboxes",
|
|
21
|
+
"cleanup_sandbox",
|
|
22
|
+
"clear_sandbox_cache",
|
|
23
|
+
"create_sandbox_backend",
|
|
24
|
+
"get_backend_class",
|
|
25
|
+
"get_backend_registry",
|
|
26
|
+
"get_sandbox_backend",
|
|
27
|
+
"register_backend",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __getattr__(name: str) -> Any:
|
|
32
|
+
"""Lazy-load backend classes to avoid circular imports and unnecessary dependencies."""
|
|
33
|
+
_lazy_map = {
|
|
34
|
+
"LocalShellBackend": "opscloud.backend.local",
|
|
35
|
+
"OpsCloudCompositeBackend": "opscloud.backend.composite",
|
|
36
|
+
"CompositeBackend": "opscloud.backend.composite",
|
|
37
|
+
"SandboxConfig": "opscloud.backend.sandbox_config",
|
|
38
|
+
"get_sandbox_backend": "opscloud.backend.sandbox_factory",
|
|
39
|
+
"create_sandbox_backend": "opscloud.backend.sandbox_factory",
|
|
40
|
+
"cleanup_sandbox": "opscloud.backend.sandbox_factory",
|
|
41
|
+
"cleanup_all_sandboxes": "opscloud.backend.sandbox_factory",
|
|
42
|
+
"clear_sandbox_cache": "opscloud.backend.sandbox_factory",
|
|
43
|
+
}
|
|
44
|
+
if name in _lazy_map:
|
|
45
|
+
import importlib
|
|
46
|
+
|
|
47
|
+
mod = importlib.import_module(_lazy_map[name])
|
|
48
|
+
return getattr(mod, name)
|
|
49
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Composite backend routing for OpsCloud.
|
|
2
|
+
|
|
3
|
+
Routes special virtual paths (``/large_tool_results/`` and ``/conversation_history/``)
|
|
4
|
+
to specialized backends while delegating all standard filesystem and command
|
|
5
|
+
execution to the default backend (e.g. ``LocalShellBackend`` or remote sandbox).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import atexit
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from deepagents.backends import (
|
|
17
|
+
BackendProtocol,
|
|
18
|
+
CompositeBackend as SDKCompositeBackend,
|
|
19
|
+
FilesystemBackend,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from opscloud.config.paths import ensure_conversation_history_dir
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OpsCloudCompositeBackend(SDKCompositeBackend):
|
|
26
|
+
"""Composite backend routing virtual paths (/large_tool_results/, /conversation_history/)."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
default: Any,
|
|
31
|
+
routes: dict[str, Any] | None = None,
|
|
32
|
+
**kwargs: Any,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize OpsCloudCompositeBackend with default and routed backends.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
default: Default backend instance to fall back to.
|
|
38
|
+
routes: Optional mapping of virtual path prefixes to backend instances.
|
|
39
|
+
**kwargs: Additional keyword arguments for SDKCompositeBackend.
|
|
40
|
+
"""
|
|
41
|
+
self._large_results_dir = tempfile.mkdtemp(prefix="opscloud_large_results_")
|
|
42
|
+
self._conversation_history_dir = ensure_conversation_history_dir()
|
|
43
|
+
|
|
44
|
+
atexit.register(self.cleanup)
|
|
45
|
+
|
|
46
|
+
large_results_backend = FilesystemBackend(
|
|
47
|
+
root_dir=self._large_results_dir,
|
|
48
|
+
virtual_mode=True,
|
|
49
|
+
)
|
|
50
|
+
conversation_history_backend = FilesystemBackend(
|
|
51
|
+
root_dir=self._conversation_history_dir,
|
|
52
|
+
virtual_mode=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
effective_routes: dict[str, BackendProtocol] = {
|
|
56
|
+
"/large_tool_results/": large_results_backend,
|
|
57
|
+
"/conversation_history/": conversation_history_backend,
|
|
58
|
+
}
|
|
59
|
+
if routes:
|
|
60
|
+
effective_routes.update(routes)
|
|
61
|
+
|
|
62
|
+
super().__init__(
|
|
63
|
+
default=default,
|
|
64
|
+
routes=effective_routes,
|
|
65
|
+
**kwargs,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def cleanup(self) -> None:
|
|
69
|
+
"""Clean up temporary large tool result directories on shutdown.
|
|
70
|
+
|
|
71
|
+
Note: Conversation history is persistent and is intentionally preserved.
|
|
72
|
+
"""
|
|
73
|
+
if hasattr(self, "_large_results_dir") and self._large_results_dir:
|
|
74
|
+
shutil.rmtree(self._large_results_dir, ignore_errors=True)
|
|
75
|
+
|
|
76
|
+
def __enter__(self) -> OpsCloudCompositeBackend:
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
80
|
+
self.cleanup()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Alias for convenience
|
|
84
|
+
CompositeBackend = OpsCloudCompositeBackend
|
|
85
|
+
|
|
86
|
+
__all__ = [
|
|
87
|
+
"CompositeBackend",
|
|
88
|
+
"OpsCloudCompositeBackend",
|
|
89
|
+
]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Local shell and filesystem execution backend for AWS operations.
|
|
2
|
+
|
|
3
|
+
Extends ``LocalShellBackend`` with a curated shell environment that preserves
|
|
4
|
+
AWS CLI, AWS CDK, Terraform/OpenTofu, Terragrunt, Kubernetes, and DevOps tool
|
|
5
|
+
credentials and environment configurations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from deepagents.backends import LocalShellBackend as SDKLocalShellBackend
|
|
15
|
+
|
|
16
|
+
from opscloud.backend.registry import register_backend
|
|
17
|
+
from opscloud.config.paths import AWS_PRESERVE_ENV_VARS
|
|
18
|
+
|
|
19
|
+
# Tool directories to prepend to PATH for CLI tool discovery
|
|
20
|
+
COMMON_TOOL_DIRS: tuple[str, ...] = (
|
|
21
|
+
"/opt/homebrew/bin",
|
|
22
|
+
"/usr/local/bin",
|
|
23
|
+
"~/.local/bin",
|
|
24
|
+
"~/.cargo/bin",
|
|
25
|
+
"~/.aws/bin",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Prefix matching for AWS and Cloud IaC ecosystem tools
|
|
29
|
+
ALLOWED_ENV_PREFIXES: tuple[str, ...] = (
|
|
30
|
+
"AWS_",
|
|
31
|
+
"TF_VAR_",
|
|
32
|
+
"TF_",
|
|
33
|
+
"TERRAGRUNT_",
|
|
34
|
+
"KUBE_",
|
|
35
|
+
"HELM_",
|
|
36
|
+
"CDK_",
|
|
37
|
+
"PULUMI_",
|
|
38
|
+
"SAM_",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_shell_env() -> dict[str, str]:
|
|
43
|
+
"""Curate environment preserving safe shell vars, tool PATHs, and AWS/cloud credentials."""
|
|
44
|
+
safe_keys = {
|
|
45
|
+
"PATH",
|
|
46
|
+
"HOME",
|
|
47
|
+
"SHELL",
|
|
48
|
+
"TERM",
|
|
49
|
+
"LANG",
|
|
50
|
+
"USER",
|
|
51
|
+
"LOGNAME",
|
|
52
|
+
"PWD",
|
|
53
|
+
"EDITOR",
|
|
54
|
+
"VISUAL",
|
|
55
|
+
"LC_ALL",
|
|
56
|
+
"LOCALE",
|
|
57
|
+
}
|
|
58
|
+
env: dict[str, str] = {}
|
|
59
|
+
for key in safe_keys:
|
|
60
|
+
if key in os.environ:
|
|
61
|
+
env[key] = os.environ[key]
|
|
62
|
+
|
|
63
|
+
# Prepend standard binary locations to PATH
|
|
64
|
+
path_val = env.get("PATH", "")
|
|
65
|
+
for tool_dir in COMMON_TOOL_DIRS:
|
|
66
|
+
expanded = os.path.expanduser(tool_dir)
|
|
67
|
+
if expanded not in path_val.split(os.pathsep):
|
|
68
|
+
path_val = f"{expanded}{os.pathsep}{path_val}" if path_val else expanded
|
|
69
|
+
env["PATH"] = path_val
|
|
70
|
+
|
|
71
|
+
# Ensure AWS_PAGER is disabled by default so aws CLI commands do not hang waiting for paging
|
|
72
|
+
if "AWS_PAGER" in os.environ:
|
|
73
|
+
env["AWS_PAGER"] = os.environ["AWS_PAGER"]
|
|
74
|
+
else:
|
|
75
|
+
env["AWS_PAGER"] = ""
|
|
76
|
+
|
|
77
|
+
# Prevent interactive terminal prompts in subshells (e.g. Git credentials, CLI pagers)
|
|
78
|
+
env["GIT_TERMINAL_PROMPT"] = os.environ.get("GIT_TERMINAL_PROMPT", "0")
|
|
79
|
+
if "PAGER" not in env:
|
|
80
|
+
env["PAGER"] = os.environ.get("PAGER", "cat")
|
|
81
|
+
|
|
82
|
+
# Preserve all AWS & Cloud IaC environment variables
|
|
83
|
+
for key, val in os.environ.items():
|
|
84
|
+
if key in AWS_PRESERVE_ENV_VARS or key.startswith(ALLOWED_ENV_PREFIXES):
|
|
85
|
+
env[key] = val
|
|
86
|
+
|
|
87
|
+
return env
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@register_backend("local")
|
|
91
|
+
class LocalShellBackend(SDKLocalShellBackend):
|
|
92
|
+
"""Local shell and filesystem backend specialized for AWS DevOps/SRE operations."""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
root_dir: Path | str,
|
|
97
|
+
virtual_mode: bool = False,
|
|
98
|
+
inherit_env: bool = False,
|
|
99
|
+
env: dict[str, str] | None = None,
|
|
100
|
+
**kwargs: Any,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Initialize LocalShellBackend with sanitized AWS cloud environment.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
root_dir: Root directory for file and command operations.
|
|
106
|
+
virtual_mode: Whether to restrict operations to virtual filesystem.
|
|
107
|
+
inherit_env: Whether to inherit parent process environment variables.
|
|
108
|
+
env: Explicit environment variables dictionary.
|
|
109
|
+
**kwargs: Additional keyword arguments for SDKLocalShellBackend.
|
|
110
|
+
"""
|
|
111
|
+
effective_env = env if env is not None else _build_shell_env()
|
|
112
|
+
super().__init__(
|
|
113
|
+
root_dir=Path(root_dir),
|
|
114
|
+
virtual_mode=virtual_mode,
|
|
115
|
+
inherit_env=inherit_env,
|
|
116
|
+
env=effective_env,
|
|
117
|
+
**kwargs,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _sync_cloud_env(self) -> None:
|
|
121
|
+
"""Dynamically sync active AWS profile and credentials into the shell execution environment.
|
|
122
|
+
|
|
123
|
+
Reads latest persisted cloud profile from config.toml, ambient AWS_PROFILE,
|
|
124
|
+
and synchronizes AWS_PROFILE, AWS_DEFAULT_PROFILE, and region.
|
|
125
|
+
When an explicit named profile is active, purges any static conflicting
|
|
126
|
+
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY from self._env so AWS CLI and boto3
|
|
127
|
+
properly use the profile's authentic credentials rather than failing.
|
|
128
|
+
"""
|
|
129
|
+
from opscloud.config.cloud_profiles import get_active_aws_profile, get_active_aws_region
|
|
130
|
+
|
|
131
|
+
active_profile = get_active_aws_profile()
|
|
132
|
+
if active_profile:
|
|
133
|
+
self._env["AWS_PROFILE"] = active_profile
|
|
134
|
+
self._env["AWS_DEFAULT_PROFILE"] = active_profile
|
|
135
|
+
os.environ["AWS_PROFILE"] = active_profile
|
|
136
|
+
os.environ["AWS_DEFAULT_PROFILE"] = active_profile
|
|
137
|
+
|
|
138
|
+
# Clear static credentials when a named profile is selected so AWS CLI
|
|
139
|
+
# authentication precedence does not prioritize stale or unauthenticated env keys.
|
|
140
|
+
if active_profile != "default":
|
|
141
|
+
self._env.pop("AWS_ACCESS_KEY_ID", None)
|
|
142
|
+
self._env.pop("AWS_SECRET_ACCESS_KEY", None)
|
|
143
|
+
self._env.pop("AWS_SESSION_TOKEN", None)
|
|
144
|
+
|
|
145
|
+
active_region = get_active_aws_region()
|
|
146
|
+
if active_region:
|
|
147
|
+
self._env["AWS_REGION"] = active_region
|
|
148
|
+
self._env["AWS_DEFAULT_REGION"] = active_region
|
|
149
|
+
os.environ["AWS_REGION"] = active_region
|
|
150
|
+
os.environ["AWS_DEFAULT_REGION"] = active_region
|
|
151
|
+
else:
|
|
152
|
+
self._env.pop("AWS_REGION", None)
|
|
153
|
+
self._env.pop("AWS_DEFAULT_REGION", None)
|
|
154
|
+
os.environ.pop("AWS_REGION", None)
|
|
155
|
+
os.environ.pop("AWS_DEFAULT_REGION", None)
|
|
156
|
+
|
|
157
|
+
def execute(
|
|
158
|
+
self,
|
|
159
|
+
command: str,
|
|
160
|
+
*,
|
|
161
|
+
timeout: int | None = None,
|
|
162
|
+
) -> Any:
|
|
163
|
+
"""Execute a shell command with dynamically synchronized cloud profile environment."""
|
|
164
|
+
self._sync_cloud_env()
|
|
165
|
+
return super().execute(command, timeout=timeout)
|
|
166
|
+
|
|
167
|
+
async def aexecute(
|
|
168
|
+
self,
|
|
169
|
+
command: str,
|
|
170
|
+
*,
|
|
171
|
+
timeout: int | None = None,
|
|
172
|
+
) -> Any:
|
|
173
|
+
"""Asynchronously execute shell command with dynamically synchronized cloud profile environment."""
|
|
174
|
+
self._sync_cloud_env()
|
|
175
|
+
return await super().aexecute(command, timeout=timeout)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = [
|
|
179
|
+
"ALLOWED_ENV_PREFIXES",
|
|
180
|
+
"COMMON_TOOL_DIRS",
|
|
181
|
+
"LocalShellBackend",
|
|
182
|
+
"_build_shell_env",
|
|
183
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Singleton registry for backend providers in OpsCloud."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
import threading
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound=type)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BackendRegistry:
|
|
13
|
+
"""Singleton registry for backend providers."""
|
|
14
|
+
|
|
15
|
+
_instance: BackendRegistry | None = None
|
|
16
|
+
_lock = threading.Lock()
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
"""Initialize BackendRegistry with an empty provider mapping."""
|
|
20
|
+
self._providers: dict[str, type] = {}
|
|
21
|
+
|
|
22
|
+
def register(self, name: str, cls: type) -> None:
|
|
23
|
+
"""Register a backend provider class under *name*."""
|
|
24
|
+
self._providers[name] = cls
|
|
25
|
+
|
|
26
|
+
def build(self, name: str, **kwargs: Any) -> Any:
|
|
27
|
+
"""Instantiate a registered backend by *name*."""
|
|
28
|
+
cls = self._providers.get(name)
|
|
29
|
+
if cls is None:
|
|
30
|
+
raise ValueError(f"Backend provider '{name}' is not registered.")
|
|
31
|
+
return cls(**kwargs)
|
|
32
|
+
|
|
33
|
+
def list_registered(self) -> list[str]:
|
|
34
|
+
"""Return sorted list of registered backend names."""
|
|
35
|
+
return sorted(self._providers.keys())
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_instance(cls) -> BackendRegistry:
|
|
39
|
+
"""Return the singleton instance, creating it if needed."""
|
|
40
|
+
if cls._instance is None:
|
|
41
|
+
with cls._lock:
|
|
42
|
+
if cls._instance is None:
|
|
43
|
+
cls._instance = cls()
|
|
44
|
+
return cls._instance
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_backend_registry() -> BackendRegistry:
|
|
48
|
+
"""Module-level accessor for the singleton ``BackendRegistry``."""
|
|
49
|
+
return BackendRegistry.get_instance()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def register_backend(name: str) -> Callable[[T], T]:
|
|
53
|
+
"""Class decorator to register a backend provider.
|
|
54
|
+
|
|
55
|
+
Usage::
|
|
56
|
+
|
|
57
|
+
@register_backend("local")
|
|
58
|
+
class LocalShellBackend(SDKLocalShellBackend):
|
|
59
|
+
...
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def decorator(cls: T) -> T:
|
|
63
|
+
get_backend_registry().register(name, cls)
|
|
64
|
+
return cls
|
|
65
|
+
|
|
66
|
+
return decorator
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_backend_class(name: str) -> type | None:
|
|
70
|
+
"""Return the registered backend class for *name*, or None."""
|
|
71
|
+
return get_backend_registry()._providers.get(name)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"BackendRegistry",
|
|
76
|
+
"get_backend_class",
|
|
77
|
+
"get_backend_registry",
|
|
78
|
+
"register_backend",
|
|
79
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Configuration model for sandboxed backend execution environments in OpsCloud."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class SandboxConfig:
|
|
13
|
+
"""Settings required to provision, configure, and connect to a sandbox environment."""
|
|
14
|
+
|
|
15
|
+
provider: str = "local"
|
|
16
|
+
"""Target provider identifier ('local', 'langsmith', 'daytona', 'modal', 'runloop', 'vercel', 'agentcore')."""
|
|
17
|
+
|
|
18
|
+
sandbox_id: str | None = None
|
|
19
|
+
"""Optional existing remote sandbox ID to reconnect to."""
|
|
20
|
+
|
|
21
|
+
image: str | None = None
|
|
22
|
+
"""Optional container base image name or Dockerfile ref."""
|
|
23
|
+
|
|
24
|
+
setup_script_path: str | None = None
|
|
25
|
+
"""Optional path to a shell script to run inside the sandbox on boot."""
|
|
26
|
+
|
|
27
|
+
snapshot_name: str | None = None
|
|
28
|
+
"""Optional snapshot or blueprint name to boot from (supported by LangSmith/Runloop)."""
|
|
29
|
+
|
|
30
|
+
sync_workspace: bool = True
|
|
31
|
+
"""Whether to automatically synchronize local project files into the sandbox (default True)."""
|
|
32
|
+
|
|
33
|
+
providers: dict[str, Any] = field(default_factory=dict)
|
|
34
|
+
"""Optional provider-specific configurations parsed from [sandboxes.<provider>]."""
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def load(cls, config_path: Path | None = None) -> SandboxConfig:
|
|
38
|
+
"""Load sandbox configuration with layered precedence: Env > TOML > Defaults."""
|
|
39
|
+
from opscloud.config.toml_config import read_config_toml
|
|
40
|
+
|
|
41
|
+
toml_data = read_config_toml(config_path)
|
|
42
|
+
sandboxes_table = toml_data.get("sandboxes", {})
|
|
43
|
+
if not isinstance(sandboxes_table, dict):
|
|
44
|
+
sandboxes_table = {}
|
|
45
|
+
|
|
46
|
+
# Provider / default
|
|
47
|
+
env_provider = os.getenv("OPSCLOUD_SANDBOX_PROVIDER") or os.getenv("SANDBOX_PROVIDER")
|
|
48
|
+
toml_provider = sandboxes_table.get("default") or sandboxes_table.get("provider")
|
|
49
|
+
provider = env_provider or toml_provider or "local"
|
|
50
|
+
|
|
51
|
+
# Sandbox ID
|
|
52
|
+
env_sandbox_id = os.getenv("OPSCLOUD_SANDBOX_ID") or os.getenv("SANDBOX_ID")
|
|
53
|
+
toml_sandbox_id = sandboxes_table.get("sandbox_id")
|
|
54
|
+
sandbox_id = env_sandbox_id or toml_sandbox_id
|
|
55
|
+
|
|
56
|
+
# Image
|
|
57
|
+
env_image = os.getenv("OPSCLOUD_SANDBOX_IMAGE") or os.getenv("SANDBOX_IMAGE")
|
|
58
|
+
toml_image = sandboxes_table.get("image")
|
|
59
|
+
image = env_image or toml_image
|
|
60
|
+
|
|
61
|
+
# Setup script path
|
|
62
|
+
env_setup_script = os.getenv("OPSCLOUD_SANDBOX_SETUP_SCRIPT") or os.getenv("SANDBOX_SETUP_SCRIPT")
|
|
63
|
+
toml_setup_script = sandboxes_table.get("setup_script_path")
|
|
64
|
+
setup_script_path = env_setup_script or toml_setup_script
|
|
65
|
+
|
|
66
|
+
# Snapshot name
|
|
67
|
+
env_snapshot = os.getenv("OPSCLOUD_SANDBOX_SNAPSHOT") or os.getenv("SANDBOX_SNAPSHOT")
|
|
68
|
+
toml_snapshot = sandboxes_table.get("snapshot_name") or sandboxes_table.get("snapshot")
|
|
69
|
+
snapshot_name = env_snapshot or toml_snapshot
|
|
70
|
+
|
|
71
|
+
# Sync workspace
|
|
72
|
+
sync_raw = os.getenv("OPSCLOUD_SANDBOX_SYNC_WORKSPACE") or os.getenv("SANDBOX_SYNC_WORKSPACE")
|
|
73
|
+
if sync_raw is not None:
|
|
74
|
+
sync_workspace = sync_raw.lower() not in ("false", "0", "no")
|
|
75
|
+
elif "sync_workspace" in sandboxes_table:
|
|
76
|
+
val = sandboxes_table["sync_workspace"]
|
|
77
|
+
sync_workspace = bool(val) if not isinstance(val, str) else val.lower() not in ("false", "0", "no")
|
|
78
|
+
else:
|
|
79
|
+
sync_workspace = True
|
|
80
|
+
|
|
81
|
+
# Extract nested provider tables (e.g. [sandboxes.daytona])
|
|
82
|
+
nested_providers: dict[str, Any] = {}
|
|
83
|
+
for k, v in sandboxes_table.items():
|
|
84
|
+
if isinstance(v, dict):
|
|
85
|
+
nested_providers[k] = v
|
|
86
|
+
|
|
87
|
+
provider_clean = str(provider).lower().strip()
|
|
88
|
+
if provider_clean in ("none", ""):
|
|
89
|
+
provider_clean = "local"
|
|
90
|
+
|
|
91
|
+
return cls(
|
|
92
|
+
provider=provider_clean,
|
|
93
|
+
sandbox_id=str(sandbox_id) if sandbox_id else None,
|
|
94
|
+
image=str(image) if image else None,
|
|
95
|
+
setup_script_path=str(setup_script_path) if setup_script_path else None,
|
|
96
|
+
snapshot_name=str(snapshot_name) if snapshot_name else None,
|
|
97
|
+
sync_workspace=sync_workspace,
|
|
98
|
+
providers=nested_providers,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_env(cls) -> SandboxConfig:
|
|
103
|
+
"""Load sandbox configuration (delegates to load())."""
|
|
104
|
+
return cls.load()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ["SandboxConfig"]
|