zjcode 0.0.1__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.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Sandbox provider interface used by Deep Agents Code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from deepagents.backends.protocol import SandboxBackendProtocol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class SandboxInstallHint:
|
|
16
|
+
"""How to install the package that provides a sandbox backend.
|
|
17
|
+
|
|
18
|
+
Built-in providers ship as `deepagents-code` extras (`kind="extra"`);
|
|
19
|
+
third-party providers install as arbitrary packages (`kind="package"`).
|
|
20
|
+
The distinction lets error messages emit the correct install command
|
|
21
|
+
(`/install daytona` vs. `/install acme-dcode-sandbox --package`).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
kind: Literal["extra", "package"]
|
|
25
|
+
name: str
|
|
26
|
+
|
|
27
|
+
def command(self, *, in_app: bool) -> str:
|
|
28
|
+
"""Render the install command for this hint.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
in_app: Whether to render the in-app slash command (`/install`)
|
|
32
|
+
rather than the CLI command (`dcode --install`).
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The install command string.
|
|
36
|
+
"""
|
|
37
|
+
prefix = "/install" if in_app else "dcode --install"
|
|
38
|
+
suffix = " --package" if self.kind == "package" else ""
|
|
39
|
+
return f"{prefix} {self.name}{suffix}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class SandboxProviderMetadata:
|
|
44
|
+
"""Static description of a sandbox provider used by the registry.
|
|
45
|
+
|
|
46
|
+
Lets the CLI and registry describe built-in and config providers without
|
|
47
|
+
instantiating them (which may require credentials or optional
|
|
48
|
+
dependencies). Entry-point providers expose their own instance via the
|
|
49
|
+
`SandboxProvider.metadata` property, which the registry reads only when it
|
|
50
|
+
already needs to construct the provider.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
working_dir: str
|
|
55
|
+
install: SandboxInstallHint | None = None
|
|
56
|
+
supports_sandbox_id: bool = True
|
|
57
|
+
supports_snapshot_name: bool = False
|
|
58
|
+
backend_module: str | None = None
|
|
59
|
+
"""Importable backend module checked by the pre-flight dependency probe.
|
|
60
|
+
|
|
61
|
+
`None` skips the probe (e.g. bundled providers, or third-party providers
|
|
62
|
+
whose package is only resolved when the provider is constructed).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SandboxError(Exception):
|
|
67
|
+
"""Base error for sandbox provider operations."""
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def original_exc(self) -> BaseException | None:
|
|
71
|
+
"""Original exception that caused this error, if any."""
|
|
72
|
+
return self.__cause__
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SandboxNotFoundError(SandboxError):
|
|
76
|
+
"""Raised when the requested sandbox cannot be found."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SandboxProvider(ABC):
|
|
80
|
+
"""Interface for creating and deleting sandbox backends."""
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def metadata(self) -> SandboxProviderMetadata | None:
|
|
84
|
+
"""Static metadata describing this provider.
|
|
85
|
+
|
|
86
|
+
Third-party providers published under the
|
|
87
|
+
`deepagents_code.sandbox_providers` entry-point group override this so
|
|
88
|
+
the registry can surface their working directory and capability flags
|
|
89
|
+
(snapshot/sandbox-id support) instead of falling back to a generic
|
|
90
|
+
placeholder. Returns `None` by default; the registry then synthesizes a
|
|
91
|
+
minimal default.
|
|
92
|
+
"""
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def get_or_create(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
sandbox_id: str | None = None,
|
|
100
|
+
**kwargs: Any,
|
|
101
|
+
) -> SandboxBackendProtocol:
|
|
102
|
+
"""Get an existing sandbox, or create one if needed."""
|
|
103
|
+
raise NotImplementedError
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def delete(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
sandbox_id: str,
|
|
110
|
+
**kwargs: Any,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Delete a sandbox by id."""
|
|
113
|
+
raise NotImplementedError
|
|
114
|
+
|
|
115
|
+
async def aget_or_create(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
sandbox_id: str | None = None,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> SandboxBackendProtocol:
|
|
121
|
+
"""Async wrapper around get_or_create.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The created or existing sandbox backend.
|
|
125
|
+
"""
|
|
126
|
+
return await asyncio.to_thread(
|
|
127
|
+
self.get_or_create, sandbox_id=sandbox_id, **kwargs
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def adelete(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
sandbox_id: str,
|
|
134
|
+
**kwargs: Any,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Async wrapper around delete."""
|
|
137
|
+
await asyncio.to_thread(self.delete, sandbox_id=sandbox_id, **kwargs)
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Discovery and instantiation of sandbox providers.
|
|
2
|
+
|
|
3
|
+
Merges three provider sources into one registry:
|
|
4
|
+
|
|
5
|
+
1. Built-in providers curated in this repo (installed as `deepagents-code`
|
|
6
|
+
extras).
|
|
7
|
+
2. Entry-point providers published by third-party packages under the
|
|
8
|
+
`deepagents_code.sandbox_providers` group.
|
|
9
|
+
3. Config-declared providers from `[sandboxes.providers]` in
|
|
10
|
+
`~/.deepagents/config.toml` (escape hatch for internal/local packages).
|
|
11
|
+
|
|
12
|
+
Precedence on name collision: config > entry point > built-in, so a user can
|
|
13
|
+
always override discovery via their config file.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import importlib
|
|
19
|
+
import importlib.metadata
|
|
20
|
+
import logging
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from deepagents_code.integrations.sandbox_config import SandboxConfig
|
|
24
|
+
from deepagents_code.integrations.sandbox_provider import (
|
|
25
|
+
SandboxInstallHint,
|
|
26
|
+
SandboxProvider,
|
|
27
|
+
SandboxProviderMetadata,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
ENTRY_POINT_GROUP = "deepagents_code.sandbox_providers"
|
|
36
|
+
"""Entry-point group third-party packages publish providers under."""
|
|
37
|
+
|
|
38
|
+
BUILTIN_METADATA: dict[str, SandboxProviderMetadata] = {
|
|
39
|
+
"agentcore": SandboxProviderMetadata(
|
|
40
|
+
name="agentcore",
|
|
41
|
+
working_dir="/tmp", # noqa: S108 # AgentCore Code Interpreter working directory
|
|
42
|
+
install=SandboxInstallHint(kind="extra", name="agentcore"),
|
|
43
|
+
supports_sandbox_id=False,
|
|
44
|
+
backend_module="langchain_agentcore_codeinterpreter",
|
|
45
|
+
),
|
|
46
|
+
"daytona": SandboxProviderMetadata(
|
|
47
|
+
name="daytona",
|
|
48
|
+
working_dir="/home/daytona",
|
|
49
|
+
install=SandboxInstallHint(kind="extra", name="daytona"),
|
|
50
|
+
backend_module="langchain_daytona",
|
|
51
|
+
),
|
|
52
|
+
"langsmith": SandboxProviderMetadata(
|
|
53
|
+
name="langsmith",
|
|
54
|
+
working_dir="/root", # `$HOME` in the LangSmith sandbox
|
|
55
|
+
# Bundled with `deepagents-code` via `langsmith[sandbox]`; no extra.
|
|
56
|
+
supports_snapshot_name=True,
|
|
57
|
+
),
|
|
58
|
+
"modal": SandboxProviderMetadata(
|
|
59
|
+
name="modal",
|
|
60
|
+
working_dir="/workspace",
|
|
61
|
+
install=SandboxInstallHint(kind="extra", name="modal"),
|
|
62
|
+
backend_module="langchain_modal",
|
|
63
|
+
),
|
|
64
|
+
"runloop": SandboxProviderMetadata(
|
|
65
|
+
name="runloop",
|
|
66
|
+
working_dir="/home/user",
|
|
67
|
+
install=SandboxInstallHint(kind="extra", name="runloop"),
|
|
68
|
+
supports_snapshot_name=True,
|
|
69
|
+
backend_module="langchain_runloop",
|
|
70
|
+
),
|
|
71
|
+
"vercel": SandboxProviderMetadata(
|
|
72
|
+
name="vercel",
|
|
73
|
+
working_dir="/vercel/sandbox",
|
|
74
|
+
install=SandboxInstallHint(kind="extra", name="vercel"),
|
|
75
|
+
supports_sandbox_id=True,
|
|
76
|
+
supports_snapshot_name=False,
|
|
77
|
+
backend_module="langchain_vercel_sandbox",
|
|
78
|
+
),
|
|
79
|
+
}
|
|
80
|
+
"""Metadata for curated built-in providers, keyed by provider name."""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _load_class(class_path: str) -> type:
|
|
84
|
+
"""Import a `module.path:ClassName` provider class.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
class_path: Fully-qualified class path.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
The imported class object.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ValueError: If `class_path` is malformed.
|
|
94
|
+
ImportError: If the module cannot be imported or lacks the class.
|
|
95
|
+
"""
|
|
96
|
+
if ":" not in class_path:
|
|
97
|
+
msg = (
|
|
98
|
+
f"Invalid class_path '{class_path}': must be in "
|
|
99
|
+
"module.path:ClassName format"
|
|
100
|
+
)
|
|
101
|
+
raise ValueError(msg)
|
|
102
|
+
module_path, class_name = class_path.rsplit(":", 1)
|
|
103
|
+
module = importlib.import_module(module_path)
|
|
104
|
+
cls = getattr(module, class_name, None)
|
|
105
|
+
if cls is None or not isinstance(cls, type):
|
|
106
|
+
msg = f"Class '{class_name}' not found in module '{module_path}'"
|
|
107
|
+
raise ImportError(msg)
|
|
108
|
+
return cls
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _provider_metadata(provider: SandboxProvider, name: str) -> SandboxProviderMetadata:
|
|
112
|
+
"""Extract metadata from a provider instance or class.
|
|
113
|
+
|
|
114
|
+
Providers may expose a `metadata` attribute/property; otherwise a minimal
|
|
115
|
+
default is synthesized.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
provider: Provider instance.
|
|
119
|
+
name: Provider name to use when synthesizing defaults.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The provider's metadata.
|
|
123
|
+
"""
|
|
124
|
+
meta = getattr(provider, "metadata", None)
|
|
125
|
+
if isinstance(meta, SandboxProviderMetadata):
|
|
126
|
+
return meta
|
|
127
|
+
return SandboxProviderMetadata(name=name, working_dir="/workspace")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class SandboxRegistry:
|
|
131
|
+
"""Merged view of built-in, entry-point, and config sandbox providers."""
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
config: SandboxConfig | None = None,
|
|
137
|
+
include_entry_points: bool = True,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Build the registry.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
config: Parsed sandbox config. Loaded from the default path when
|
|
143
|
+
omitted.
|
|
144
|
+
include_entry_points: Whether to discover entry-point providers.
|
|
145
|
+
Disabled in tests that need a deterministic provider set.
|
|
146
|
+
"""
|
|
147
|
+
self._config = config if config is not None else SandboxConfig.load()
|
|
148
|
+
self._include_entry_points = include_entry_points
|
|
149
|
+
self._entry_points: dict[str, importlib.metadata.EntryPoint] = (
|
|
150
|
+
self._discover_entry_points() if include_entry_points else {}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def load(cls, config_path: Path | None = None) -> SandboxRegistry:
|
|
155
|
+
"""Build a registry from the config file at `config_path`.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
config_path: Path to config file. Defaults to the user config.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
A new `SandboxRegistry`.
|
|
162
|
+
"""
|
|
163
|
+
return cls(config=SandboxConfig.load(config_path))
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _discover_entry_points() -> dict[str, importlib.metadata.EntryPoint]:
|
|
167
|
+
"""Return entry-point providers keyed by name (best-effort)."""
|
|
168
|
+
found: dict[str, importlib.metadata.EntryPoint] = {}
|
|
169
|
+
try:
|
|
170
|
+
entries = importlib.metadata.entry_points(group=ENTRY_POINT_GROUP)
|
|
171
|
+
except Exception:
|
|
172
|
+
logger.warning("Failed to discover sandbox entry points", exc_info=True)
|
|
173
|
+
return found
|
|
174
|
+
for entry in entries:
|
|
175
|
+
found[entry.name] = entry
|
|
176
|
+
return found
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def default(self) -> str | None:
|
|
180
|
+
"""The configured default provider, if any."""
|
|
181
|
+
return self._config.default
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def config_error(self) -> str | None:
|
|
185
|
+
"""Why the config file failed to load, if it existed but couldn't parse.
|
|
186
|
+
|
|
187
|
+
`None` when the config parsed cleanly or simply wasn't present.
|
|
188
|
+
"""
|
|
189
|
+
return self._config.parse_error
|
|
190
|
+
|
|
191
|
+
def available_providers(self) -> list[str]:
|
|
192
|
+
"""Return all known provider names, sorted."""
|
|
193
|
+
names = (
|
|
194
|
+
set(BUILTIN_METADATA)
|
|
195
|
+
| set(self._entry_points)
|
|
196
|
+
| set(self._config.providers)
|
|
197
|
+
)
|
|
198
|
+
return sorted(names)
|
|
199
|
+
|
|
200
|
+
def is_available(self, name: str) -> bool:
|
|
201
|
+
"""Return whether `name` resolves to a known provider."""
|
|
202
|
+
return (
|
|
203
|
+
name in self._config.providers
|
|
204
|
+
or name in self._entry_points
|
|
205
|
+
or name in BUILTIN_METADATA
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def get_metadata(self, name: str) -> SandboxProviderMetadata | None:
|
|
209
|
+
"""Return metadata for `name`.
|
|
210
|
+
|
|
211
|
+
Config providers may override `working_dir`, `package`, and capability
|
|
212
|
+
flags. Entry-point providers are probed for advertised metadata and
|
|
213
|
+
take precedence over built-ins with the same name.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
name: Provider name.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The provider's metadata, or `None` if unknown.
|
|
220
|
+
"""
|
|
221
|
+
config_entry = self._config.providers.get(name)
|
|
222
|
+
if config_entry is not None:
|
|
223
|
+
base = BUILTIN_METADATA.get(name)
|
|
224
|
+
package = config_entry.get("package")
|
|
225
|
+
return SandboxProviderMetadata(
|
|
226
|
+
name=name,
|
|
227
|
+
working_dir=config_entry.get(
|
|
228
|
+
"working_dir", base.working_dir if base else "/workspace"
|
|
229
|
+
),
|
|
230
|
+
install=(
|
|
231
|
+
SandboxInstallHint(kind="package", name=package)
|
|
232
|
+
if package
|
|
233
|
+
else (base.install if base else None)
|
|
234
|
+
),
|
|
235
|
+
supports_sandbox_id=config_entry.get(
|
|
236
|
+
"supports_sandbox_id",
|
|
237
|
+
base.supports_sandbox_id if base else True,
|
|
238
|
+
),
|
|
239
|
+
supports_snapshot_name=config_entry.get(
|
|
240
|
+
"supports_snapshot_name",
|
|
241
|
+
base.supports_snapshot_name if base else False,
|
|
242
|
+
),
|
|
243
|
+
# Carry the built-in's probe module so a config override of a
|
|
244
|
+
# built-in keeps its dependency pre-flight check. A pure config
|
|
245
|
+
# provider (no base) leaves this `None`; its package is resolved
|
|
246
|
+
# when the provider class is imported.
|
|
247
|
+
backend_module=base.backend_module if base else None,
|
|
248
|
+
)
|
|
249
|
+
if name in self._entry_points:
|
|
250
|
+
return self.provider_metadata(name)
|
|
251
|
+
if name in BUILTIN_METADATA:
|
|
252
|
+
return BUILTIN_METADATA[name]
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
def get_params(self, name: str) -> dict[str, object]:
|
|
256
|
+
"""Return config `params` forwarded to the provider's `get_or_create()`."""
|
|
257
|
+
return self._config.get_params(name)
|
|
258
|
+
|
|
259
|
+
def create_provider(self, name: str) -> SandboxProvider:
|
|
260
|
+
"""Instantiate the provider named `name`.
|
|
261
|
+
|
|
262
|
+
Resolution order: config `class_path` > entry point > built-in.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
name: Provider name.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
A `SandboxProvider` instance. Propagates `ImportError` from
|
|
269
|
+
`_load_class` / `EntryPoint.load` if a config or entry-point
|
|
270
|
+
class cannot be imported.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
ValueError: If `name` is unknown or a config provider omits
|
|
274
|
+
`class_path`.
|
|
275
|
+
"""
|
|
276
|
+
config_entry = self._config.providers.get(name)
|
|
277
|
+
if config_entry is not None:
|
|
278
|
+
class_path = config_entry.get("class_path")
|
|
279
|
+
if not class_path:
|
|
280
|
+
msg = f"Sandbox provider '{name}' config is missing 'class_path'"
|
|
281
|
+
raise ValueError(msg)
|
|
282
|
+
return _load_class(class_path)()
|
|
283
|
+
|
|
284
|
+
entry = self._entry_points.get(name)
|
|
285
|
+
if entry is not None:
|
|
286
|
+
return entry.load()()
|
|
287
|
+
|
|
288
|
+
if name in BUILTIN_METADATA:
|
|
289
|
+
return _create_builtin_provider(name)
|
|
290
|
+
|
|
291
|
+
msg = (
|
|
292
|
+
f"Unknown sandbox provider: {name}. "
|
|
293
|
+
f"Available providers: {', '.join(self.available_providers())}"
|
|
294
|
+
)
|
|
295
|
+
raise ValueError(msg)
|
|
296
|
+
|
|
297
|
+
def provider_metadata(self, name: str) -> SandboxProviderMetadata:
|
|
298
|
+
"""Return authoritative metadata for `name`.
|
|
299
|
+
|
|
300
|
+
Config providers are described statically. Entry-point providers are
|
|
301
|
+
instantiated so capability flags they expose via a `metadata` attribute
|
|
302
|
+
take effect; on failure this falls back to the static placeholder.
|
|
303
|
+
Built-in metadata is used only when no entry point overrides that name.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
name: Provider name.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
The provider's metadata.
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
ValueError: If `name` is unknown.
|
|
313
|
+
"""
|
|
314
|
+
if name in self._config.providers or (
|
|
315
|
+
name in BUILTIN_METADATA and name not in self._entry_points
|
|
316
|
+
):
|
|
317
|
+
meta = self.get_metadata(name)
|
|
318
|
+
if meta is not None:
|
|
319
|
+
return meta
|
|
320
|
+
if name in self._entry_points:
|
|
321
|
+
try:
|
|
322
|
+
provider = self.create_provider(name)
|
|
323
|
+
except Exception: # noqa: BLE001 # Metadata probe must not crash discovery
|
|
324
|
+
logger.debug("Could not instantiate provider %r for metadata", name)
|
|
325
|
+
return SandboxProviderMetadata(name=name, working_dir="/workspace")
|
|
326
|
+
return _provider_metadata(provider, name)
|
|
327
|
+
meta = self.get_metadata(name)
|
|
328
|
+
if meta is None:
|
|
329
|
+
msg = f"Unknown sandbox provider: {name}"
|
|
330
|
+
raise ValueError(msg)
|
|
331
|
+
return meta
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _create_builtin_provider(name: str) -> SandboxProvider:
|
|
335
|
+
"""Instantiate a built-in provider class (lazy import avoids cycles).
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
The built-in `SandboxProvider` instance for `name`.
|
|
339
|
+
"""
|
|
340
|
+
from deepagents_code.integrations import sandbox_factory
|
|
341
|
+
|
|
342
|
+
builders = {
|
|
343
|
+
"agentcore": sandbox_factory._AgentCoreProvider,
|
|
344
|
+
"daytona": sandbox_factory._DaytonaProvider,
|
|
345
|
+
"langsmith": sandbox_factory._LangSmithProvider,
|
|
346
|
+
"modal": sandbox_factory._ModalProvider,
|
|
347
|
+
"runloop": sandbox_factory._RunloopProvider,
|
|
348
|
+
"vercel": sandbox_factory._VercelProvider,
|
|
349
|
+
}
|
|
350
|
+
return builders[name]()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""iTerm2 cursor guide workaround for Textual alternate-screen rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# iTerm2's cursor guide (highlight cursor line) causes visual artifacts when
|
|
9
|
+
# Textual takes over the terminal in alternate screen mode. We disable it at
|
|
10
|
+
# module load and restore it on exit only if the active/default iTerm2 profile
|
|
11
|
+
# had cursor guide enabled before launch.
|
|
12
|
+
|
|
13
|
+
# Detection: check env vars AND that stderr is a TTY (avoids false positives
|
|
14
|
+
# when env vars are inherited but running in non-TTY context like CI).
|
|
15
|
+
_IS_ITERM = (
|
|
16
|
+
(
|
|
17
|
+
os.environ.get("LC_TERMINAL", "") == "iTerm2"
|
|
18
|
+
or os.environ.get("TERM_PROGRAM", "") == "iTerm.app"
|
|
19
|
+
)
|
|
20
|
+
and hasattr(os, "isatty")
|
|
21
|
+
and os.isatty(2)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# iTerm2 cursor guide escape sequences (OSC 1337)
|
|
25
|
+
# Format: OSC 1337 ; HighlightCursorLine=<yes|no> ST
|
|
26
|
+
# Where OSC = ESC ] (0x1b 0x5d) and ST = ESC \ (0x1b 0x5c)
|
|
27
|
+
_ITERM_CURSOR_GUIDE_OFF = "\x1b]1337;HighlightCursorLine=no\x1b\\"
|
|
28
|
+
_ITERM_CURSOR_GUIDE_ON = "\x1b]1337;HighlightCursorLine=yes\x1b\\"
|
|
29
|
+
_ITERM_PREFS_PATH = Path("~/Library/Preferences/com.googlecode.iterm2.plist")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _write_iterm_escape(sequence: str) -> None:
|
|
33
|
+
"""Write an iTerm2 escape sequence to stderr.
|
|
34
|
+
|
|
35
|
+
Silently fails if the terminal is unavailable (redirected, closed, broken
|
|
36
|
+
pipe). This is a cosmetic feature, so failures should never crash the app.
|
|
37
|
+
"""
|
|
38
|
+
if not _IS_ITERM:
|
|
39
|
+
return
|
|
40
|
+
try:
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
if sys.__stderr__ is not None:
|
|
44
|
+
sys.__stderr__.write(sequence)
|
|
45
|
+
sys.__stderr__.flush()
|
|
46
|
+
except OSError:
|
|
47
|
+
# Terminal may be unavailable (redirected, closed, broken pipe).
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _plist_bool(value: object) -> bool | None:
|
|
52
|
+
"""Return a plist boolean/int value as `bool`, or `None` if not boolean-like."""
|
|
53
|
+
if isinstance(value, bool):
|
|
54
|
+
return value
|
|
55
|
+
if isinstance(value, int):
|
|
56
|
+
return value != 0
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _profile_uses_cursor_guide(profile: dict[str, object]) -> bool:
|
|
61
|
+
"""Return whether an iTerm2 profile has cursor guide enabled."""
|
|
62
|
+
enabled = _plist_bool(profile.get("Use Cursor Guide"))
|
|
63
|
+
if enabled is not None:
|
|
64
|
+
return enabled
|
|
65
|
+
|
|
66
|
+
# Newer iTerm2 profiles may carry separate light/dark values. If the shared
|
|
67
|
+
# value is absent, restoring when either variant is enabled preserves the
|
|
68
|
+
# user's visible default better than losing the guide for both appearances.
|
|
69
|
+
return any(
|
|
70
|
+
_plist_bool(profile.get(key)) is True
|
|
71
|
+
for key in ("Use Cursor Guide (Dark)", "Use Cursor Guide (Light)")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _coerce_profile(raw: object) -> dict[str, object] | None:
|
|
76
|
+
"""Return a string-keyed profile dictionary from raw plist data."""
|
|
77
|
+
if not isinstance(raw, dict):
|
|
78
|
+
return None
|
|
79
|
+
return {key: value for key, value in raw.items() if isinstance(key, str)}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _find_iterm_profile(
|
|
83
|
+
profiles: list[object], *, name: str, guid: str
|
|
84
|
+
) -> dict[str, object] | None:
|
|
85
|
+
"""Find the current iTerm2 profile by name, then by default profile GUID.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
profiles: Profile entries from iTerm2 preferences.
|
|
89
|
+
name: Active profile name from `ITERM_PROFILE`.
|
|
90
|
+
guid: Default profile GUID from iTerm2 preferences.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The matching profile dictionary, or `None` when no match is found.
|
|
94
|
+
"""
|
|
95
|
+
for raw in profiles:
|
|
96
|
+
profile = _coerce_profile(raw)
|
|
97
|
+
if profile is None:
|
|
98
|
+
continue
|
|
99
|
+
if profile.get("Name") == name:
|
|
100
|
+
return profile
|
|
101
|
+
for raw in profiles:
|
|
102
|
+
profile = _coerce_profile(raw)
|
|
103
|
+
if profile is None:
|
|
104
|
+
continue
|
|
105
|
+
if profile.get("Guid") == guid:
|
|
106
|
+
return profile
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _iterm_profile_cursor_guide_enabled() -> bool:
|
|
111
|
+
"""Infer whether iTerm2 cursor guide was enabled before startup.
|
|
112
|
+
|
|
113
|
+
iTerm2's OSC 1337 `HighlightCursorLine` command can set the guide to yes/no
|
|
114
|
+
but does not report the current state. The best cheap signal available at
|
|
115
|
+
startup is the active profile preference, exposed in the iTerm2 plist. The
|
|
116
|
+
`ITERM_PROFILE` environment variable is set by iTerm2; when it is missing,
|
|
117
|
+
fall back to the default profile GUID in preferences.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
`True` if the matched iTerm2 profile has cursor guide enabled.
|
|
121
|
+
"""
|
|
122
|
+
if not _IS_ITERM:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
import plistlib
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
with _ITERM_PREFS_PATH.expanduser().open("rb") as f:
|
|
129
|
+
prefs = plistlib.load(f)
|
|
130
|
+
except (OSError, plistlib.InvalidFileException, ValueError):
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
if not isinstance(prefs, dict):
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
profiles = prefs.get("New Bookmarks")
|
|
137
|
+
if not isinstance(profiles, list):
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
name = os.environ.get("ITERM_PROFILE", "").strip()
|
|
141
|
+
guid = str(prefs.get("Default Bookmark Guid", ""))
|
|
142
|
+
profile = _find_iterm_profile(profiles, name=name, guid=guid)
|
|
143
|
+
if profile is None:
|
|
144
|
+
return False
|
|
145
|
+
return _profile_uses_cursor_guide(profile)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_RESTORE_ITERM_CURSOR_GUIDE = _iterm_profile_cursor_guide_enabled()
|
|
149
|
+
_ITERM_CURSOR_GUIDE_RESTORED = False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def restore_iterm_cursor_guide() -> None:
|
|
153
|
+
"""Restore iTerm2 cursor guide when launch-time profile state required it."""
|
|
154
|
+
global _ITERM_CURSOR_GUIDE_RESTORED # noqa: PLW0603 # atexit/exit idempotence
|
|
155
|
+
|
|
156
|
+
if not _RESTORE_ITERM_CURSOR_GUIDE or _ITERM_CURSOR_GUIDE_RESTORED:
|
|
157
|
+
return
|
|
158
|
+
_ITERM_CURSOR_GUIDE_RESTORED = True
|
|
159
|
+
_write_iterm_escape(_ITERM_CURSOR_GUIDE_ON)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _disable_iterm_cursor_guide() -> None:
|
|
163
|
+
"""Disable iTerm2 cursor guide only when the module has a restore path."""
|
|
164
|
+
if not _RESTORE_ITERM_CURSOR_GUIDE:
|
|
165
|
+
return
|
|
166
|
+
_write_iterm_escape(_ITERM_CURSOR_GUIDE_OFF)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Disable cursor guide at module load (before Textual takes over), but only
|
|
170
|
+
# when launch-time state detection confirmed that exit cleanup will restore it.
|
|
171
|
+
_disable_iterm_cursor_guide()
|
|
172
|
+
|
|
173
|
+
if _RESTORE_ITERM_CURSOR_GUIDE:
|
|
174
|
+
import atexit
|
|
175
|
+
|
|
176
|
+
atexit.register(restore_iterm_cursor_guide)
|