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