databricks-agentbricks 0.2.0__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.
- databricks_agentbricks/__init__.py +1 -0
- databricks_agentbricks/_group.py +217 -0
- databricks_agentbricks/agent_project.py +657 -0
- databricks_agentbricks/app_resources.py +139 -0
- databricks_agentbricks/cli/__init__.py +12 -0
- databricks_agentbricks/cli/app.py +108 -0
- databricks_agentbricks/cli/app_auth.py +198 -0
- databricks_agentbricks/cli/auth.py +142 -0
- databricks_agentbricks/cli/deploy.py +954 -0
- databricks_agentbricks/cli/dev.py +310 -0
- databricks_agentbricks/cli/endpoint.py +145 -0
- databricks_agentbricks/cli/endpoint_examples.py +33 -0
- databricks_agentbricks/cli/endpoint_output.py +53 -0
- databricks_agentbricks/cli/endpoint_request.py +71 -0
- databricks_agentbricks/cli/endpoint_transport.py +148 -0
- databricks_agentbricks/cli/help.py +467 -0
- databricks_agentbricks/cli/init.py +496 -0
- databricks_agentbricks/cli/mcp.py +75 -0
- databricks_agentbricks/cli/memory.py +520 -0
- databricks_agentbricks/cli/sandbox.py +823 -0
- databricks_agentbricks/cli/sessions.py +588 -0
- databricks_agentbricks/cli/tools.py +456 -0
- databricks_agentbricks/cli/tracing.py +724 -0
- databricks_agentbricks/databricks_cli.py +38 -0
- databricks_agentbricks/errors.py +114 -0
- databricks_agentbricks/lakebase_runtime_store.py +107 -0
- databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
- databricks_agentbricks/project_config.py +157 -0
- databricks_agentbricks/project_types.py +55 -0
- databricks_agentbricks/py.typed +0 -0
- databricks_agentbricks/render.py +349 -0
- databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
- databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
- databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
- databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
- databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
- databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
- databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
- databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/agent-openai/.env.example +37 -0
- databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
- databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
- databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
- databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-openai/README.md +143 -0
- databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
- databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
- databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
- databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
- databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
- databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
- databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/sandbox_mcp.py +39 -0
- databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
- databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
- databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
- databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
- databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
- databricks_agentbricks/theme.py +72 -0
- databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
- databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
- databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
- databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
- databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
- databricks_agentkit/__init__.py +67 -0
- databricks_agentkit/_api_client.py +673 -0
- databricks_agentkit/_pagination.py +25 -0
- databricks_agentkit/client.py +31 -0
- databricks_agentkit/langgraph/__init__.py +98 -0
- databricks_agentkit/langgraph/genie.py +50 -0
- databricks_agentkit/langgraph/mcp.py +216 -0
- databricks_agentkit/langgraph/memory.py +67 -0
- databricks_agentkit/langgraph/session_store.py +485 -0
- databricks_agentkit/memory_store.py +371 -0
- databricks_agentkit/models.py +330 -0
- databricks_agentkit/openai/__init__.py +93 -0
- databricks_agentkit/openai/genie.py +50 -0
- databricks_agentkit/openai/mcp.py +189 -0
- databricks_agentkit/openai/memory.py +67 -0
- databricks_agentkit/openai/sessions.py +157 -0
- databricks_agentkit/py.typed +0 -0
- databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
- databricks_agentkit/runtime/README.md +184 -0
- databricks_agentkit/runtime/__init__.py +69 -0
- databricks_agentkit/runtime/app.py +316 -0
- databricks_agentkit/runtime/auth.py +149 -0
- databricks_agentkit/runtime/durability/__init__.py +1 -0
- databricks_agentkit/runtime/durability/execution.py +133 -0
- databricks_agentkit/runtime/durability/heartbeat.py +64 -0
- databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
- databricks_agentkit/runtime/durability/recovery.py +70 -0
- databricks_agentkit/runtime/durability/store.py +33 -0
- databricks_agentkit/runtime/execution.py +160 -0
- databricks_agentkit/runtime/genie.py +224 -0
- databricks_agentkit/runtime/mcp_auth.py +114 -0
- databricks_agentkit/runtime/model_services.py +90 -0
- databricks_agentkit/runtime/runtime.py +273 -0
- databricks_agentkit/runtime/session_store_client.py +120 -0
- databricks_agentkit/runtime/store.py +287 -0
- databricks_agentkit/runtime/tool_manifest.py +223 -0
- databricks_agentkit/runtime/tracing.py +71 -0
- databricks_agentkit/runtime/types.py +118 -0
- databricks_agentkit/runtime/workspace.py +27 -0
- databricks_agentkit/session_store.py +434 -0
- databricks_agentkit/timefmt.py +85 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
"""``ab add-sandbox`` — attach a downscoped ``system.ai.sandbox`` MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import json
|
|
7
|
+
import pathlib
|
|
8
|
+
import stat
|
|
9
|
+
import tempfile
|
|
10
|
+
import textwrap
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from importlib import resources
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
from databricks_agentbricks import render
|
|
18
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
19
|
+
from databricks_agentbricks.project_config import load_project_metadata
|
|
20
|
+
from databricks_agentbricks.project_types import AgentFramework
|
|
21
|
+
|
|
22
|
+
_BEGIN_MARKER = "# BEGIN: agentbricks add-sandbox"
|
|
23
|
+
_END_MARKER = "# END: agentbricks add-sandbox"
|
|
24
|
+
_DOWNSCOPE_PLACEHOLDER = "# __AGENTBRICKS_SANDBOX_DOWNSCOPE__"
|
|
25
|
+
_SERVER_FACTORY_CALL = "_build_sandbox_mcp_server()"
|
|
26
|
+
_OPENAI_TEMPLATE_NAME = "sandbox_mcp.py"
|
|
27
|
+
_LANGGRAPH_TEMPLATE_NAME = "sandbox_mcp_langgraph.py"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _validate_uc_name(value: str, resource_type: str) -> str:
|
|
31
|
+
if (
|
|
32
|
+
len(value.split(".")) != 3
|
|
33
|
+
or any(not part for part in value.split("."))
|
|
34
|
+
or any(character.isspace() for character in value)
|
|
35
|
+
):
|
|
36
|
+
raise AgentCliError(
|
|
37
|
+
f"Invalid {resource_type} scope '{value}'.",
|
|
38
|
+
hint=f"Use a three-part Unity Catalog name: catalog.schema.{resource_type}.",
|
|
39
|
+
)
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _parse_scopes(scopes: Sequence[str], permission: str) -> dict[str, list[dict[str, str]]]:
|
|
44
|
+
"""Convert CLI scope values to the MCP ``_meta.downscope`` wire shape."""
|
|
45
|
+
parsed: dict[str, list[dict[str, str]]] = {
|
|
46
|
+
"volumes": [],
|
|
47
|
+
"tables": [],
|
|
48
|
+
"workspace_paths": [],
|
|
49
|
+
}
|
|
50
|
+
seen: set[tuple[str, str]] = set()
|
|
51
|
+
|
|
52
|
+
for original in scopes:
|
|
53
|
+
value = original.strip()
|
|
54
|
+
if not value:
|
|
55
|
+
raise AgentCliError("Sandbox scopes cannot be empty.")
|
|
56
|
+
|
|
57
|
+
prefix, separator, remainder = value.partition(":")
|
|
58
|
+
explicit_type = prefix if separator and prefix in {"volume", "table", "workspace"} else None
|
|
59
|
+
if explicit_type:
|
|
60
|
+
value = remainder.strip()
|
|
61
|
+
|
|
62
|
+
if explicit_type == "workspace" or (
|
|
63
|
+
explicit_type is None and value.startswith("/Workspace/")
|
|
64
|
+
):
|
|
65
|
+
if not value.startswith("/Workspace/") or any(
|
|
66
|
+
character in value for character in ("\r", "\n", "\t")
|
|
67
|
+
):
|
|
68
|
+
raise AgentCliError(
|
|
69
|
+
f"Invalid workspace scope '{value}'.",
|
|
70
|
+
hint="Workspace paths must begin with /Workspace/ and cannot contain tabs or newlines.",
|
|
71
|
+
)
|
|
72
|
+
key, field = "workspace_paths", "path"
|
|
73
|
+
else:
|
|
74
|
+
resource_type = explicit_type or "volume"
|
|
75
|
+
if resource_type == "workspace": # Handled above; keeps the type narrow below.
|
|
76
|
+
raise AssertionError("unreachable")
|
|
77
|
+
value = _validate_uc_name(value, resource_type)
|
|
78
|
+
key, field = ("tables", "name") if resource_type == "table" else ("volumes", "name")
|
|
79
|
+
|
|
80
|
+
identity = (key, value)
|
|
81
|
+
if identity not in seen:
|
|
82
|
+
parsed[key].append({field: value, "permission": permission})
|
|
83
|
+
seen.add(identity)
|
|
84
|
+
|
|
85
|
+
return {key: values for key, values in parsed.items() if values}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _offset(lines: list[str], line: int, column: int) -> int:
|
|
89
|
+
try:
|
|
90
|
+
line_prefix = lines[line - 1].encode("utf-8")[:column].decode("utf-8")
|
|
91
|
+
except (IndexError, UnicodeDecodeError) as exc:
|
|
92
|
+
raise AgentCliError(
|
|
93
|
+
"Could not locate the generated Python syntax in agent/mcps.py."
|
|
94
|
+
) from exc
|
|
95
|
+
return sum(len(part) for part in lines[: line - 1]) + len(line_prefix)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _end_position(node: ast.expr | ast.stmt) -> tuple[int, int]:
|
|
99
|
+
line = node.end_lineno
|
|
100
|
+
column = node.end_col_offset
|
|
101
|
+
if line is None or column is None:
|
|
102
|
+
raise AgentCliError("Could not locate the generated Python syntax in agent/mcps.py.")
|
|
103
|
+
return line, column
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _find_server_list(tree: ast.Module) -> tuple[ast.FunctionDef, ast.List]:
|
|
107
|
+
functions = [
|
|
108
|
+
node
|
|
109
|
+
for node in tree.body
|
|
110
|
+
if isinstance(node, ast.FunctionDef) and node.name == "build_mcp_servers"
|
|
111
|
+
]
|
|
112
|
+
if len(functions) != 1:
|
|
113
|
+
raise AgentCliError(
|
|
114
|
+
"Could not find one build_mcp_servers() function in agent/mcps.py.",
|
|
115
|
+
hint="Run this command from an Agent Bricks project generated from a supported template.",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
direct_returns = [node for node in functions[0].body if isinstance(node, ast.Return)]
|
|
119
|
+
all_returns = [node for node in ast.walk(functions[0]) if isinstance(node, ast.Return)]
|
|
120
|
+
if (
|
|
121
|
+
len(direct_returns) != 1
|
|
122
|
+
or len(all_returns) != 1
|
|
123
|
+
or not isinstance(direct_returns[0].value, ast.List)
|
|
124
|
+
):
|
|
125
|
+
raise AgentCliError(
|
|
126
|
+
"build_mcp_servers() must directly return a list before Agent Bricks can add the sandbox.",
|
|
127
|
+
hint="Change the function to `return [...]`, then retry.",
|
|
128
|
+
)
|
|
129
|
+
return functions[0], direct_returns[0].value
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _append_server_to_list(source: str) -> str:
|
|
133
|
+
"""Append the generated server factory while preserving the rest of ``mcps.py``."""
|
|
134
|
+
try:
|
|
135
|
+
tree = ast.parse(source)
|
|
136
|
+
except SyntaxError as exc:
|
|
137
|
+
raise AgentCliError(
|
|
138
|
+
f"Could not parse agent/mcps.py: {exc.msg} (line {exc.lineno})."
|
|
139
|
+
) from exc
|
|
140
|
+
|
|
141
|
+
function, server_list = _find_server_list(tree)
|
|
142
|
+
lines = source.splitlines(keepends=True)
|
|
143
|
+
start = _offset(lines, server_list.lineno, server_list.col_offset)
|
|
144
|
+
list_end_line, list_end_column = _end_position(server_list)
|
|
145
|
+
end = _offset(lines, list_end_line, list_end_column)
|
|
146
|
+
item_indent = " " * (function.col_offset + 8)
|
|
147
|
+
closing_indent = " " * (function.col_offset + 4)
|
|
148
|
+
|
|
149
|
+
if not server_list.elts:
|
|
150
|
+
replacement = f"[\n{item_indent}{_SERVER_FACTORY_CALL},\n{closing_indent}]"
|
|
151
|
+
return source[:start] + replacement + source[end:]
|
|
152
|
+
|
|
153
|
+
if server_list.lineno == server_list.end_lineno:
|
|
154
|
+
existing = []
|
|
155
|
+
for element in server_list.elts:
|
|
156
|
+
segment = ast.get_source_segment(source, element)
|
|
157
|
+
if segment is None:
|
|
158
|
+
raise AgentCliError("Could not preserve the existing MCP server list.")
|
|
159
|
+
existing.append(textwrap.indent(textwrap.dedent(segment), item_indent))
|
|
160
|
+
replacement = (
|
|
161
|
+
"[\n"
|
|
162
|
+
+ ",\n".join(existing)
|
|
163
|
+
+ f",\n{item_indent}{_SERVER_FACTORY_CALL},\n{closing_indent}]"
|
|
164
|
+
)
|
|
165
|
+
return source[:start] + replacement + source[end:]
|
|
166
|
+
|
|
167
|
+
closing_line_start = _offset(lines, list_end_line, 0)
|
|
168
|
+
closing_prefix = source[closing_line_start : end - 1]
|
|
169
|
+
if closing_prefix.strip():
|
|
170
|
+
raise AgentCliError(
|
|
171
|
+
"The closing bracket of build_mcp_servers() must be on its own line.",
|
|
172
|
+
hint="Format the returned list as a standard multi-line Python list, then retry.",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
last = server_list.elts[-1]
|
|
176
|
+
last_end_line, last_end_column = _end_position(last)
|
|
177
|
+
last_end = _offset(lines, last_end_line, last_end_column)
|
|
178
|
+
suffix = source[last_end:closing_line_start]
|
|
179
|
+
comma = "" if suffix.lstrip().startswith(",") else ","
|
|
180
|
+
with_item = (
|
|
181
|
+
source[:last_end]
|
|
182
|
+
+ comma
|
|
183
|
+
+ source[last_end:closing_line_start]
|
|
184
|
+
+ f"{item_indent}{_SERVER_FACTORY_CALL},\n"
|
|
185
|
+
+ source[closing_line_start:]
|
|
186
|
+
)
|
|
187
|
+
return with_item
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _read_template(template_name: str = _OPENAI_TEMPLATE_NAME) -> str:
|
|
191
|
+
try:
|
|
192
|
+
return (
|
|
193
|
+
resources.files("databricks_agentbricks")
|
|
194
|
+
.joinpath("templates")
|
|
195
|
+
.joinpath(template_name)
|
|
196
|
+
.read_text(encoding="utf-8")
|
|
197
|
+
)
|
|
198
|
+
except (OSError, UnicodeError) as exc:
|
|
199
|
+
raise AgentCliError("Could not read the packaged sandbox MCP template.") from exc
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _insert_imports(
|
|
203
|
+
source: str,
|
|
204
|
+
template: str,
|
|
205
|
+
*,
|
|
206
|
+
required_import: tuple[str, str] = ("agents.mcp", "MCPServer"),
|
|
207
|
+
) -> str:
|
|
208
|
+
tree = ast.parse(source)
|
|
209
|
+
try:
|
|
210
|
+
template_tree = ast.parse(template)
|
|
211
|
+
except SyntaxError as exc:
|
|
212
|
+
raise AgentCliError("The packaged sandbox MCP template is invalid.") from exc
|
|
213
|
+
lines = source.splitlines(keepends=True)
|
|
214
|
+
|
|
215
|
+
def has_from_import(module: str, name: str) -> bool:
|
|
216
|
+
return any(
|
|
217
|
+
isinstance(node, ast.ImportFrom)
|
|
218
|
+
and node.module == module
|
|
219
|
+
and any((alias.asname or alias.name) == name for alias in node.names)
|
|
220
|
+
for node in tree.body
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
template_imports = [
|
|
224
|
+
node
|
|
225
|
+
for node in template_tree.body
|
|
226
|
+
if isinstance(node, ast.ImportFrom) and node.module is not None
|
|
227
|
+
]
|
|
228
|
+
if not template_imports or any(
|
|
229
|
+
node.level != 0 or len(node.names) != 1 or node.names[0].asname is not None
|
|
230
|
+
for node in template_imports
|
|
231
|
+
):
|
|
232
|
+
raise AgentCliError("The packaged sandbox MCP template has invalid imports.")
|
|
233
|
+
|
|
234
|
+
missing_imports: list[tuple[str, str]] = []
|
|
235
|
+
for node in template_imports:
|
|
236
|
+
module = node.module
|
|
237
|
+
if module is None:
|
|
238
|
+
raise AgentCliError("The packaged sandbox MCP template has invalid imports.")
|
|
239
|
+
alias = node.names[0]
|
|
240
|
+
if not has_from_import(module, alias.name):
|
|
241
|
+
segment = ast.get_source_segment(template, node)
|
|
242
|
+
if segment is None:
|
|
243
|
+
raise AgentCliError("The packaged sandbox MCP template has invalid imports.")
|
|
244
|
+
missing_imports.append((module, segment))
|
|
245
|
+
|
|
246
|
+
required_module, required_name = required_import
|
|
247
|
+
protocol_imports = [
|
|
248
|
+
node
|
|
249
|
+
for node in tree.body
|
|
250
|
+
if isinstance(node, ast.ImportFrom)
|
|
251
|
+
and node.module == required_module
|
|
252
|
+
and any((alias.asname or alias.name) == required_name for alias in node.names)
|
|
253
|
+
]
|
|
254
|
+
if len(protocol_imports) != 1:
|
|
255
|
+
raise AgentCliError(
|
|
256
|
+
f"agent/mcps.py must import {required_name} from {required_module} before Agent Bricks can "
|
|
257
|
+
"add the sandbox.",
|
|
258
|
+
hint="Start from the Agent Bricks agent template, then retry.",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
insert_after: ast.stmt | None = None
|
|
262
|
+
if (
|
|
263
|
+
tree.body
|
|
264
|
+
and isinstance(tree.body[0], ast.Expr)
|
|
265
|
+
and isinstance(tree.body[0].value, ast.Constant)
|
|
266
|
+
and isinstance(tree.body[0].value.value, str)
|
|
267
|
+
):
|
|
268
|
+
insert_after = tree.body[0]
|
|
269
|
+
|
|
270
|
+
for node in tree.body:
|
|
271
|
+
if isinstance(node, ast.ImportFrom) and node.module == "__future__":
|
|
272
|
+
insert_after = node
|
|
273
|
+
elif node is not insert_after and not (
|
|
274
|
+
isinstance(node, ast.Expr)
|
|
275
|
+
and isinstance(node.value, ast.Constant)
|
|
276
|
+
and isinstance(node.value.value, str)
|
|
277
|
+
):
|
|
278
|
+
break
|
|
279
|
+
|
|
280
|
+
with_runtime_imports = source
|
|
281
|
+
runtime_imports = sorted(
|
|
282
|
+
((module, segment) for module, segment in missing_imports if module != "typing"),
|
|
283
|
+
key=lambda item: item[0],
|
|
284
|
+
)
|
|
285
|
+
if runtime_imports:
|
|
286
|
+
import_start = _offset(lines, protocol_imports[0].lineno, 0)
|
|
287
|
+
import_end_line, import_end_column = _end_position(protocol_imports[0])
|
|
288
|
+
import_end = _offset(lines, import_end_line, import_end_column)
|
|
289
|
+
before = [segment for module, segment in runtime_imports if module < required_module]
|
|
290
|
+
after = [segment for module, segment in runtime_imports if module > required_module]
|
|
291
|
+
with_runtime_imports = (
|
|
292
|
+
source[:import_start]
|
|
293
|
+
+ ("\n".join(before) + "\n" if before else "")
|
|
294
|
+
+ source[import_start:import_end]
|
|
295
|
+
+ ("\n" + "\n".join(after) if after else "")
|
|
296
|
+
+ source[import_end:]
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
typing_imports = [segment for module, segment in missing_imports if module == "typing"]
|
|
300
|
+
if not typing_imports:
|
|
301
|
+
return with_runtime_imports
|
|
302
|
+
|
|
303
|
+
if insert_after is not None:
|
|
304
|
+
end_line, end_column = _end_position(insert_after)
|
|
305
|
+
insert_at = _offset(lines, end_line, end_column)
|
|
306
|
+
following = with_runtime_imports[insert_at:].lstrip("\n")
|
|
307
|
+
return (
|
|
308
|
+
with_runtime_imports[:insert_at]
|
|
309
|
+
+ "\n\n"
|
|
310
|
+
+ "\n".join(typing_imports)
|
|
311
|
+
+ "\n\n"
|
|
312
|
+
+ following
|
|
313
|
+
)
|
|
314
|
+
return "\n".join(typing_imports) + "\n\n" + with_runtime_imports.lstrip("\n")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _format_policy(policy: dict[str, list[dict[str, str]]]) -> str:
|
|
318
|
+
lines = ["_SANDBOX_DOWNSCOPE: dict[str, list[dict[str, str]]] = {"]
|
|
319
|
+
for key, values in policy.items():
|
|
320
|
+
lines.append(f' "{key}": [')
|
|
321
|
+
for value in values:
|
|
322
|
+
fields = ", ".join(f"{json.dumps(k)}: {json.dumps(v)}" for k, v in value.items())
|
|
323
|
+
lines.append(f" {{{fields}}},")
|
|
324
|
+
lines.append(" ],")
|
|
325
|
+
lines.append("}")
|
|
326
|
+
return "\n".join(lines)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _existing_policy(
|
|
330
|
+
source: str,
|
|
331
|
+
*,
|
|
332
|
+
template_name: str = _OPENAI_TEMPLATE_NAME,
|
|
333
|
+
) -> dict[str, list[dict[str, str]]] | None:
|
|
334
|
+
begin_count = source.count(_BEGIN_MARKER)
|
|
335
|
+
end_count = source.count(_END_MARKER)
|
|
336
|
+
if begin_count == 0 and end_count == 0:
|
|
337
|
+
return None
|
|
338
|
+
if (
|
|
339
|
+
begin_count != 1
|
|
340
|
+
or end_count != 1
|
|
341
|
+
or source.index(_BEGIN_MARKER) > source.index(_END_MARKER)
|
|
342
|
+
):
|
|
343
|
+
raise AgentCliError(
|
|
344
|
+
"The existing agentbricks add-sandbox block has invalid markers.",
|
|
345
|
+
hint="Repair or remove the generated block in agent/mcps.py, then retry.",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
tree = ast.parse(source)
|
|
350
|
+
except SyntaxError as exc:
|
|
351
|
+
raise AgentCliError(
|
|
352
|
+
f"Could not parse agent/mcps.py: {exc.msg} (line {exc.lineno})."
|
|
353
|
+
) from exc
|
|
354
|
+
|
|
355
|
+
_, server_list = _find_server_list(tree)
|
|
356
|
+
assignments = [
|
|
357
|
+
node
|
|
358
|
+
for node in tree.body
|
|
359
|
+
if isinstance(node, ast.AnnAssign)
|
|
360
|
+
and isinstance(node.target, ast.Name)
|
|
361
|
+
and node.target.id == "_SANDBOX_DOWNSCOPE"
|
|
362
|
+
and node.value is not None
|
|
363
|
+
]
|
|
364
|
+
if len(assignments) != 1:
|
|
365
|
+
raise AgentCliError(
|
|
366
|
+
"The existing agentbricks add-sandbox block is incomplete.",
|
|
367
|
+
hint="Repair or remove the generated block in agent/mcps.py, then retry.",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
assignment_value = assignments[0].value
|
|
371
|
+
if assignment_value is None:
|
|
372
|
+
raise AgentCliError("The existing sandbox downscope has no policy value.")
|
|
373
|
+
try:
|
|
374
|
+
raw_policy = ast.literal_eval(assignment_value)
|
|
375
|
+
except (ValueError, TypeError, SyntaxError) as exc:
|
|
376
|
+
raise AgentCliError(
|
|
377
|
+
"The existing sandbox downscope is not a fixed literal policy.",
|
|
378
|
+
hint="Repair or remove the generated block in agent/mcps.py, then retry.",
|
|
379
|
+
) from exc
|
|
380
|
+
|
|
381
|
+
if not isinstance(raw_policy, dict):
|
|
382
|
+
raise AgentCliError("The existing sandbox downscope is not a valid policy object.")
|
|
383
|
+
|
|
384
|
+
policy: dict[str, list[dict[str, str]]] = {}
|
|
385
|
+
for key, entries in raw_policy.items():
|
|
386
|
+
if not isinstance(key, str) or not isinstance(entries, list):
|
|
387
|
+
raise AgentCliError("The existing sandbox downscope has an invalid resource list.")
|
|
388
|
+
parsed_entries: list[dict[str, str]] = []
|
|
389
|
+
for entry in entries:
|
|
390
|
+
if not isinstance(entry, dict) or not all(
|
|
391
|
+
isinstance(field, str) and isinstance(value, str) for field, value in entry.items()
|
|
392
|
+
):
|
|
393
|
+
raise AgentCliError("The existing sandbox downscope has an invalid resource entry.")
|
|
394
|
+
parsed_entries.append(entry)
|
|
395
|
+
policy[key] = parsed_entries
|
|
396
|
+
|
|
397
|
+
expected_fields = {
|
|
398
|
+
"volumes": "name",
|
|
399
|
+
"tables": "name",
|
|
400
|
+
"workspace_paths": "path",
|
|
401
|
+
}
|
|
402
|
+
if not policy or any(key not in expected_fields for key in policy):
|
|
403
|
+
raise AgentCliError("The existing sandbox downscope has an invalid resource type.")
|
|
404
|
+
for resource_type, entries in policy.items():
|
|
405
|
+
expected_field = expected_fields[resource_type]
|
|
406
|
+
if not entries:
|
|
407
|
+
raise AgentCliError("The existing sandbox downscope has an empty resource list.")
|
|
408
|
+
for entry in entries:
|
|
409
|
+
if set(entry) != {expected_field, "permission"}:
|
|
410
|
+
raise AgentCliError("The existing sandbox downscope has an invalid resource entry.")
|
|
411
|
+
if entry["permission"] not in {"read_only", "read_write"}:
|
|
412
|
+
raise AgentCliError("The existing sandbox downscope has an invalid permission.")
|
|
413
|
+
if resource_type == "workspace_paths":
|
|
414
|
+
path = entry[expected_field]
|
|
415
|
+
if not path.startswith("/Workspace/") or any(
|
|
416
|
+
character in path for character in ("\r", "\n", "\t")
|
|
417
|
+
):
|
|
418
|
+
raise AgentCliError(
|
|
419
|
+
"The existing sandbox downscope has an invalid workspace path."
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
_validate_uc_name(entry[expected_field], resource_type.removesuffix("s"))
|
|
423
|
+
|
|
424
|
+
block_start = source.index(_BEGIN_MARKER)
|
|
425
|
+
block_end = source.index(_END_MARKER) + len(_END_MARKER)
|
|
426
|
+
try:
|
|
427
|
+
actual_tree = ast.parse(source[block_start:block_end] + "\n")
|
|
428
|
+
expected_tree = ast.parse(_generated_block(policy, template_name=template_name))
|
|
429
|
+
except SyntaxError as exc:
|
|
430
|
+
raise AgentCliError("The existing agentbricks add-sandbox block is incomplete.") from exc
|
|
431
|
+
if ast.dump(actual_tree, include_attributes=False) != ast.dump(
|
|
432
|
+
expected_tree, include_attributes=False
|
|
433
|
+
):
|
|
434
|
+
raise AgentCliError(
|
|
435
|
+
"The existing agentbricks add-sandbox block does not match the generated security wrapper.",
|
|
436
|
+
hint="Repair or remove the generated block in agent/mcps.py, then retry.",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
factory_calls = [
|
|
440
|
+
element
|
|
441
|
+
for element in server_list.elts
|
|
442
|
+
if isinstance(element, ast.Call)
|
|
443
|
+
and isinstance(element.func, ast.Name)
|
|
444
|
+
and element.func.id == "_build_sandbox_mcp_server"
|
|
445
|
+
]
|
|
446
|
+
if len(factory_calls) != 1 or factory_calls[0].args or factory_calls[0].keywords:
|
|
447
|
+
raise AgentCliError(
|
|
448
|
+
"build_mcp_servers() must register exactly one generated sandbox server.",
|
|
449
|
+
hint="Repair the generated factory entry in agent/mcps.py, then retry.",
|
|
450
|
+
)
|
|
451
|
+
return policy
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _policy_signature(
|
|
455
|
+
policy: dict[str, list[dict[str, str]]],
|
|
456
|
+
) -> frozenset[tuple[str, str, str]]:
|
|
457
|
+
return frozenset(
|
|
458
|
+
(resource_type, entry.get("name", entry.get("path", "")), entry.get("permission", ""))
|
|
459
|
+
for resource_type, entries in policy.items()
|
|
460
|
+
for entry in entries
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _policy_cli_values(
|
|
465
|
+
policy: dict[str, list[dict[str, str]]],
|
|
466
|
+
) -> tuple[list[str], str]:
|
|
467
|
+
scopes: list[str] = []
|
|
468
|
+
permissions: set[str] = set()
|
|
469
|
+
for resource_type, entries in policy.items():
|
|
470
|
+
for entry in entries:
|
|
471
|
+
value = entry.get("path") or entry.get("name", "")
|
|
472
|
+
scopes.append(f"table:{value}" if resource_type == "tables" else value)
|
|
473
|
+
permissions.add(entry.get("permission", ""))
|
|
474
|
+
if len(permissions) != 1:
|
|
475
|
+
raise AgentCliError("The existing sandbox downscope has inconsistent permissions.")
|
|
476
|
+
return scopes, permissions.pop()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _write_text_atomic(target: pathlib.Path, content: str) -> None:
|
|
480
|
+
temporary: pathlib.Path | None = None
|
|
481
|
+
try:
|
|
482
|
+
mode = stat.S_IMODE(target.stat().st_mode)
|
|
483
|
+
with tempfile.NamedTemporaryFile(
|
|
484
|
+
"w",
|
|
485
|
+
encoding="utf-8",
|
|
486
|
+
dir=target.parent,
|
|
487
|
+
prefix=f".{target.name}.",
|
|
488
|
+
delete=False,
|
|
489
|
+
) as output:
|
|
490
|
+
temporary = pathlib.Path(output.name)
|
|
491
|
+
output.write(content)
|
|
492
|
+
temporary.chmod(mode)
|
|
493
|
+
temporary.replace(target)
|
|
494
|
+
temporary = None
|
|
495
|
+
except OSError as exc:
|
|
496
|
+
raise AgentCliError(f"Could not update {target}: {exc}.") from exc
|
|
497
|
+
finally:
|
|
498
|
+
if temporary is not None:
|
|
499
|
+
try:
|
|
500
|
+
temporary.unlink(missing_ok=True)
|
|
501
|
+
except OSError:
|
|
502
|
+
pass
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _generated_block(
|
|
506
|
+
policy: dict[str, list[dict[str, str]]],
|
|
507
|
+
template: str | None = None,
|
|
508
|
+
*,
|
|
509
|
+
template_name: str = _OPENAI_TEMPLATE_NAME,
|
|
510
|
+
) -> str:
|
|
511
|
+
template = template if template is not None else _read_template(template_name)
|
|
512
|
+
try:
|
|
513
|
+
tree = ast.parse(template)
|
|
514
|
+
except SyntaxError as exc:
|
|
515
|
+
raise AgentCliError("The packaged sandbox MCP template is invalid.") from exc
|
|
516
|
+
assignments = [
|
|
517
|
+
node
|
|
518
|
+
for node in tree.body
|
|
519
|
+
if isinstance(node, ast.AnnAssign)
|
|
520
|
+
and isinstance(node.target, ast.Name)
|
|
521
|
+
and node.target.id == "_SANDBOX_DOWNSCOPE"
|
|
522
|
+
]
|
|
523
|
+
lines = template.splitlines(keepends=True)
|
|
524
|
+
if (
|
|
525
|
+
len(assignments) != 1
|
|
526
|
+
or assignments[0].lineno != assignments[0].end_lineno
|
|
527
|
+
or _DOWNSCOPE_PLACEHOLDER not in lines[assignments[0].lineno - 1]
|
|
528
|
+
):
|
|
529
|
+
raise AgentCliError("The packaged sandbox MCP template is invalid.")
|
|
530
|
+
line_start = _offset(lines, assignments[0].lineno, 0)
|
|
531
|
+
line_end = line_start + len(lines[assignments[0].lineno - 1])
|
|
532
|
+
rendered = template[:line_start] + _format_policy(policy) + "\n" + template[line_end:]
|
|
533
|
+
if rendered.count(_BEGIN_MARKER) != 1 or rendered.count(_END_MARKER) != 1:
|
|
534
|
+
raise AgentCliError("The packaged sandbox MCP template has invalid markers.")
|
|
535
|
+
block_start = rendered.index(_BEGIN_MARKER)
|
|
536
|
+
block_end = rendered.index(_END_MARKER) + len(_END_MARKER)
|
|
537
|
+
return rendered[block_start:block_end] + "\n"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _add_sandbox_to_source(
|
|
541
|
+
source: str,
|
|
542
|
+
policy: dict[str, list[dict[str, str]]],
|
|
543
|
+
*,
|
|
544
|
+
template_name: str = _OPENAI_TEMPLATE_NAME,
|
|
545
|
+
required_import: tuple[str, str] = ("agents.mcp", "MCPServer"),
|
|
546
|
+
) -> str:
|
|
547
|
+
template = _read_template(template_name)
|
|
548
|
+
with_server = _append_server_to_list(source)
|
|
549
|
+
with_imports = _insert_imports(with_server, template, required_import=required_import)
|
|
550
|
+
generated = with_imports.rstrip() + "\n\n\n" + _generated_block(policy, template)
|
|
551
|
+
try:
|
|
552
|
+
ast.parse(generated)
|
|
553
|
+
except SyntaxError as exc:
|
|
554
|
+
raise AgentCliError(
|
|
555
|
+
f"Refusing to write invalid generated Python: {exc.msg} (line {exc.lineno})."
|
|
556
|
+
) from exc
|
|
557
|
+
return generated
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _read_source(target: pathlib.Path) -> str:
|
|
561
|
+
try:
|
|
562
|
+
return target.read_text(encoding="utf-8")
|
|
563
|
+
except (OSError, UnicodeError) as exc:
|
|
564
|
+
raise AgentCliError(f"Could not read {target}: {exc}.") from exc
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _effective_policy(
|
|
568
|
+
existing_policy: dict[str, list[dict[str, str]]] | None,
|
|
569
|
+
requested_policy: dict[str, list[dict[str, str]]],
|
|
570
|
+
) -> dict[str, list[dict[str, str]]]:
|
|
571
|
+
if existing_policy is not None and _policy_signature(existing_policy) != _policy_signature(
|
|
572
|
+
requested_policy
|
|
573
|
+
):
|
|
574
|
+
raise AgentCliError(
|
|
575
|
+
"system.ai.sandbox is already configured with a different downscope.",
|
|
576
|
+
hint="Edit or remove the generated add-sandbox block in agent/mcps.py before changing scopes.",
|
|
577
|
+
)
|
|
578
|
+
return existing_policy or requested_policy
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _configure_langgraph_runtime(source: str) -> str:
|
|
582
|
+
"""Pass the generated fixed-meta interceptor into the LangGraph MCP client."""
|
|
583
|
+
try:
|
|
584
|
+
tree = ast.parse(source)
|
|
585
|
+
except SyntaxError as exc:
|
|
586
|
+
raise AgentCliError(
|
|
587
|
+
f"Could not parse agent/agentbricks/mcp_runtime.py: {exc.msg} (line {exc.lineno})."
|
|
588
|
+
) from exc
|
|
589
|
+
|
|
590
|
+
imports = [
|
|
591
|
+
node
|
|
592
|
+
for node in tree.body
|
|
593
|
+
if isinstance(node, ast.ImportFrom)
|
|
594
|
+
and node.module == "agent.mcps"
|
|
595
|
+
and any(alias.name == "build_mcp_servers" for alias in node.names)
|
|
596
|
+
]
|
|
597
|
+
if len(imports) != 1 or imports[0].lineno != imports[0].end_lineno:
|
|
598
|
+
raise AgentCliError(
|
|
599
|
+
"agent/agentbricks/mcp_runtime.py must import build_mcp_servers from agent.mcps.",
|
|
600
|
+
hint="Start from the Agent Bricks LangGraph agent template, then retry.",
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
configured = any(alias.name == "_sandbox_tool_interceptor" for alias in imports[0].names)
|
|
604
|
+
with_import = source
|
|
605
|
+
if not configured:
|
|
606
|
+
lines = source.splitlines(keepends=True)
|
|
607
|
+
import_start = _offset(lines, imports[0].lineno, imports[0].col_offset)
|
|
608
|
+
import_end_line, import_end_column = _end_position(imports[0])
|
|
609
|
+
import_end = _offset(lines, import_end_line, import_end_column)
|
|
610
|
+
if any(alias.asname is not None for alias in imports[0].names):
|
|
611
|
+
raise AgentCliError(
|
|
612
|
+
"agent/agentbricks/mcp_runtime.py must import build_mcp_servers without an alias."
|
|
613
|
+
)
|
|
614
|
+
names = sorted({alias.name for alias in imports[0].names} | {"_sandbox_tool_interceptor"})
|
|
615
|
+
replacement = f"from agent.mcps import {', '.join(names)}"
|
|
616
|
+
with_import = source[:import_start] + replacement + source[import_end:]
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
updated_tree = ast.parse(with_import)
|
|
620
|
+
except SyntaxError as exc:
|
|
621
|
+
raise AgentCliError("Could not update the LangGraph MCP runtime import.") from exc
|
|
622
|
+
client_calls = [
|
|
623
|
+
node
|
|
624
|
+
for node in ast.walk(updated_tree)
|
|
625
|
+
if isinstance(node, ast.Call)
|
|
626
|
+
and isinstance(node.func, ast.Name)
|
|
627
|
+
and node.func.id == "DatabricksMultiServerMCPClient"
|
|
628
|
+
]
|
|
629
|
+
if len(client_calls) != 1:
|
|
630
|
+
raise AgentCliError(
|
|
631
|
+
"Could not find one DatabricksMultiServerMCPClient() call in "
|
|
632
|
+
"agent/agentbricks/mcp_runtime.py.",
|
|
633
|
+
hint="Start from the Agent Bricks LangGraph agent template, then retry.",
|
|
634
|
+
)
|
|
635
|
+
client_call = client_calls[0]
|
|
636
|
+
interceptor_keywords = [
|
|
637
|
+
keyword for keyword in client_call.keywords if keyword.arg == "tool_interceptors"
|
|
638
|
+
]
|
|
639
|
+
if interceptor_keywords:
|
|
640
|
+
value = interceptor_keywords[0].value
|
|
641
|
+
if (
|
|
642
|
+
len(interceptor_keywords) != 1
|
|
643
|
+
or not isinstance(value, ast.List)
|
|
644
|
+
or len(value.elts) != 1
|
|
645
|
+
or not isinstance(value.elts[0], ast.Name)
|
|
646
|
+
or value.elts[0].id != "_sandbox_tool_interceptor"
|
|
647
|
+
):
|
|
648
|
+
raise AgentCliError(
|
|
649
|
+
"The LangGraph MCP runtime already has a different tool_interceptors setting."
|
|
650
|
+
)
|
|
651
|
+
return with_import
|
|
652
|
+
|
|
653
|
+
lines = with_import.splitlines(keepends=True)
|
|
654
|
+
call_end_line, call_end_column = _end_position(client_call)
|
|
655
|
+
call_end = _offset(lines, call_end_line, call_end_column)
|
|
656
|
+
if call_end == 0 or with_import[call_end - 1] != ")":
|
|
657
|
+
raise AgentCliError("Could not update DatabricksMultiServerMCPClient().")
|
|
658
|
+
configured_source = (
|
|
659
|
+
with_import[: call_end - 1]
|
|
660
|
+
+ ", tool_interceptors=[_sandbox_tool_interceptor]"
|
|
661
|
+
+ with_import[call_end - 1 :]
|
|
662
|
+
)
|
|
663
|
+
try:
|
|
664
|
+
ast.parse(configured_source)
|
|
665
|
+
except SyntaxError as exc:
|
|
666
|
+
raise AgentCliError("Could not update the LangGraph MCP runtime.") from exc
|
|
667
|
+
return configured_source
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class _SandboxAdapter(Protocol):
|
|
671
|
+
def apply(
|
|
672
|
+
self,
|
|
673
|
+
project: pathlib.Path,
|
|
674
|
+
policy: dict[str, list[dict[str, str]]],
|
|
675
|
+
) -> tuple[pathlib.Path, dict[str, list[dict[str, str]]], bool]: ...
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
class _OpenAISandboxAdapter:
|
|
679
|
+
def apply(
|
|
680
|
+
self,
|
|
681
|
+
project: pathlib.Path,
|
|
682
|
+
policy: dict[str, list[dict[str, str]]],
|
|
683
|
+
) -> tuple[pathlib.Path, dict[str, list[dict[str, str]]], bool]:
|
|
684
|
+
target = project / "agent" / "mcps.py"
|
|
685
|
+
current = _read_source(target)
|
|
686
|
+
existing_policy = _existing_policy(current, template_name=_OPENAI_TEMPLATE_NAME)
|
|
687
|
+
effective_policy = _effective_policy(existing_policy, policy)
|
|
688
|
+
if existing_policy is None:
|
|
689
|
+
_write_text_atomic(
|
|
690
|
+
target,
|
|
691
|
+
_add_sandbox_to_source(
|
|
692
|
+
current,
|
|
693
|
+
policy,
|
|
694
|
+
template_name=_OPENAI_TEMPLATE_NAME,
|
|
695
|
+
required_import=("agents.mcp", "MCPServer"),
|
|
696
|
+
),
|
|
697
|
+
)
|
|
698
|
+
return target, effective_policy, existing_policy is None
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class _LangGraphSandboxAdapter:
|
|
702
|
+
def apply(
|
|
703
|
+
self,
|
|
704
|
+
project: pathlib.Path,
|
|
705
|
+
policy: dict[str, list[dict[str, str]]],
|
|
706
|
+
) -> tuple[pathlib.Path, dict[str, list[dict[str, str]]], bool]:
|
|
707
|
+
target = project / "agent" / "mcps.py"
|
|
708
|
+
runtime = project / "agent" / "agentbricks" / "mcp_runtime.py"
|
|
709
|
+
if not runtime.is_file():
|
|
710
|
+
raise AgentCliError(
|
|
711
|
+
f"Could not find {runtime}.",
|
|
712
|
+
hint="Start from the Agent Bricks LangGraph agent template, then retry.",
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
current = _read_source(target)
|
|
716
|
+
runtime_source = _read_source(runtime)
|
|
717
|
+
existing_policy = _existing_policy(current, template_name=_LANGGRAPH_TEMPLATE_NAME)
|
|
718
|
+
effective_policy = _effective_policy(existing_policy, policy)
|
|
719
|
+
configured_runtime = _configure_langgraph_runtime(runtime_source)
|
|
720
|
+
if existing_policy is not None:
|
|
721
|
+
if configured_runtime != runtime_source:
|
|
722
|
+
raise AgentCliError(
|
|
723
|
+
"The existing LangGraph sandbox configuration is missing its runtime adapter.",
|
|
724
|
+
hint="Repair or remove the generated add-sandbox block, then retry.",
|
|
725
|
+
)
|
|
726
|
+
return target, effective_policy, False
|
|
727
|
+
|
|
728
|
+
generated = _add_sandbox_to_source(
|
|
729
|
+
current,
|
|
730
|
+
policy,
|
|
731
|
+
template_name=_LANGGRAPH_TEMPLATE_NAME,
|
|
732
|
+
required_import=("databricks_langchain", "DatabricksMCPServer"),
|
|
733
|
+
)
|
|
734
|
+
_write_text_atomic(target, generated)
|
|
735
|
+
try:
|
|
736
|
+
_write_text_atomic(runtime, configured_runtime)
|
|
737
|
+
except AgentCliError:
|
|
738
|
+
_write_text_atomic(target, current)
|
|
739
|
+
raise
|
|
740
|
+
return target, effective_policy, True
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
_SANDBOX_ADAPTERS: dict[AgentFramework, _SandboxAdapter] = {
|
|
744
|
+
AgentFramework.LANGGRAPH: _LangGraphSandboxAdapter(),
|
|
745
|
+
AgentFramework.OPENAI: _OpenAISandboxAdapter(),
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
@click.command("add-sandbox")
|
|
750
|
+
@click.option(
|
|
751
|
+
"--scope",
|
|
752
|
+
"scopes",
|
|
753
|
+
multiple=True,
|
|
754
|
+
required=True,
|
|
755
|
+
help=(
|
|
756
|
+
"Allowed volume (catalog.schema.volume) or /Workspace/ path. Repeat for multiple scopes; "
|
|
757
|
+
"use table:catalog.schema.table for a table."
|
|
758
|
+
),
|
|
759
|
+
)
|
|
760
|
+
@click.option(
|
|
761
|
+
"--permission",
|
|
762
|
+
type=click.Choice(["read_only", "read_write"]),
|
|
763
|
+
default="read_only",
|
|
764
|
+
show_default=True,
|
|
765
|
+
)
|
|
766
|
+
@click.option(
|
|
767
|
+
"--source",
|
|
768
|
+
type=click.Path(exists=True, file_okay=False, path_type=pathlib.Path),
|
|
769
|
+
default=pathlib.Path("."),
|
|
770
|
+
show_default=True,
|
|
771
|
+
help="Agent Bricks project containing agent/mcps.py.",
|
|
772
|
+
)
|
|
773
|
+
@click.option(
|
|
774
|
+
"--framework",
|
|
775
|
+
type=click.Choice([framework.value for framework in AgentFramework]),
|
|
776
|
+
default=None,
|
|
777
|
+
help="Override framework detection for projects created before Agent Bricks metadata existed.",
|
|
778
|
+
)
|
|
779
|
+
@click.pass_obj
|
|
780
|
+
def add_sandbox(
|
|
781
|
+
obj,
|
|
782
|
+
scopes: tuple[str, ...],
|
|
783
|
+
permission: str,
|
|
784
|
+
source: pathlib.Path,
|
|
785
|
+
framework: str | None,
|
|
786
|
+
) -> None:
|
|
787
|
+
"""Add the system.ai.sandbox MCP with a fixed downscope to an agent project."""
|
|
788
|
+
project = source.resolve()
|
|
789
|
+
if (project / "agent.toml").is_file():
|
|
790
|
+
from databricks_agentbricks.cli.tools import add_sandbox_to_manifest
|
|
791
|
+
|
|
792
|
+
if framework is not None:
|
|
793
|
+
load_project_metadata(project, framework_override=framework)
|
|
794
|
+
add_sandbox_to_manifest(obj, project, scopes, permission)
|
|
795
|
+
return
|
|
796
|
+
|
|
797
|
+
policy = _parse_scopes(scopes, permission)
|
|
798
|
+
target = project / "agent" / "mcps.py"
|
|
799
|
+
if not target.is_file():
|
|
800
|
+
raise AgentCliError(
|
|
801
|
+
f"Could not find {target}.",
|
|
802
|
+
hint="Pass --source pointing to an Agent Bricks project containing agent/mcps.py.",
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
metadata = load_project_metadata(project, framework_override=framework)
|
|
806
|
+
adapter = _SANDBOX_ADAPTERS[metadata.framework]
|
|
807
|
+
target, effective_policy, changed = adapter.apply(project, policy)
|
|
808
|
+
effective_scopes, effective_permission = _policy_cli_values(effective_policy)
|
|
809
|
+
|
|
810
|
+
status = "added" if changed else "already_configured"
|
|
811
|
+
result = {
|
|
812
|
+
"mcp_server": "system.ai.sandbox",
|
|
813
|
+
"path": str(target),
|
|
814
|
+
"permission": effective_permission,
|
|
815
|
+
"scopes": effective_scopes,
|
|
816
|
+
"status": status,
|
|
817
|
+
}
|
|
818
|
+
if getattr(obj, "output", "text") == "json":
|
|
819
|
+
render.emit_json(result)
|
|
820
|
+
elif changed:
|
|
821
|
+
click.echo(f"Added system.ai.sandbox with fixed downscoping to {target}")
|
|
822
|
+
else:
|
|
823
|
+
click.echo(f"system.ai.sandbox is already configured in {target}")
|