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.
Files changed (159) hide show
  1. databricks_agentbricks/__init__.py +1 -0
  2. databricks_agentbricks/_group.py +217 -0
  3. databricks_agentbricks/agent_project.py +657 -0
  4. databricks_agentbricks/app_resources.py +139 -0
  5. databricks_agentbricks/cli/__init__.py +12 -0
  6. databricks_agentbricks/cli/app.py +108 -0
  7. databricks_agentbricks/cli/app_auth.py +198 -0
  8. databricks_agentbricks/cli/auth.py +142 -0
  9. databricks_agentbricks/cli/deploy.py +954 -0
  10. databricks_agentbricks/cli/dev.py +310 -0
  11. databricks_agentbricks/cli/endpoint.py +145 -0
  12. databricks_agentbricks/cli/endpoint_examples.py +33 -0
  13. databricks_agentbricks/cli/endpoint_output.py +53 -0
  14. databricks_agentbricks/cli/endpoint_request.py +71 -0
  15. databricks_agentbricks/cli/endpoint_transport.py +148 -0
  16. databricks_agentbricks/cli/help.py +467 -0
  17. databricks_agentbricks/cli/init.py +496 -0
  18. databricks_agentbricks/cli/mcp.py +75 -0
  19. databricks_agentbricks/cli/memory.py +520 -0
  20. databricks_agentbricks/cli/sandbox.py +823 -0
  21. databricks_agentbricks/cli/sessions.py +588 -0
  22. databricks_agentbricks/cli/tools.py +456 -0
  23. databricks_agentbricks/cli/tracing.py +724 -0
  24. databricks_agentbricks/databricks_cli.py +38 -0
  25. databricks_agentbricks/errors.py +114 -0
  26. databricks_agentbricks/lakebase_runtime_store.py +107 -0
  27. databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
  28. databricks_agentbricks/project_config.py +157 -0
  29. databricks_agentbricks/project_types.py +55 -0
  30. databricks_agentbricks/py.typed +0 -0
  31. databricks_agentbricks/render.py +349 -0
  32. databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
  33. databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
  34. databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
  35. databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
  36. databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
  37. databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
  38. databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
  39. databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
  40. databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
  41. databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
  42. databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
  43. databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
  44. databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
  45. databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
  46. databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
  47. databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
  48. databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
  49. databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
  50. databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
  51. databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
  52. databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
  53. databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
  54. databricks_agentbricks/templates/agent-openai/.env.example +37 -0
  55. databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
  56. databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
  57. databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
  58. databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
  59. databricks_agentbricks/templates/agent-openai/README.md +143 -0
  60. databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
  61. databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
  62. databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
  63. databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
  64. databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
  65. databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
  66. databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
  67. databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
  68. databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
  69. databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
  70. databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
  71. databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
  72. databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
  73. databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
  74. databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
  75. databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
  76. databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
  77. databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
  78. databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
  79. databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
  80. databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
  81. databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
  82. databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
  83. databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
  84. databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
  85. databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
  86. databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
  87. databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
  88. databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
  89. databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
  90. databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
  91. databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
  92. databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
  93. databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
  94. databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
  95. databricks_agentbricks/templates/sandbox_mcp.py +39 -0
  96. databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
  97. databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
  98. databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
  99. databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
  100. databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
  101. databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
  102. databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
  103. databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
  104. databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
  105. databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
  106. databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
  107. databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
  108. databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
  109. databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
  110. databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
  111. databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
  112. databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
  113. databricks_agentbricks/theme.py +72 -0
  114. databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
  115. databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
  116. databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
  117. databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
  118. databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
  119. databricks_agentkit/__init__.py +67 -0
  120. databricks_agentkit/_api_client.py +673 -0
  121. databricks_agentkit/_pagination.py +25 -0
  122. databricks_agentkit/client.py +31 -0
  123. databricks_agentkit/langgraph/__init__.py +98 -0
  124. databricks_agentkit/langgraph/genie.py +50 -0
  125. databricks_agentkit/langgraph/mcp.py +216 -0
  126. databricks_agentkit/langgraph/memory.py +67 -0
  127. databricks_agentkit/langgraph/session_store.py +485 -0
  128. databricks_agentkit/memory_store.py +371 -0
  129. databricks_agentkit/models.py +330 -0
  130. databricks_agentkit/openai/__init__.py +93 -0
  131. databricks_agentkit/openai/genie.py +50 -0
  132. databricks_agentkit/openai/mcp.py +189 -0
  133. databricks_agentkit/openai/memory.py +67 -0
  134. databricks_agentkit/openai/sessions.py +157 -0
  135. databricks_agentkit/py.typed +0 -0
  136. databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
  137. databricks_agentkit/runtime/README.md +184 -0
  138. databricks_agentkit/runtime/__init__.py +69 -0
  139. databricks_agentkit/runtime/app.py +316 -0
  140. databricks_agentkit/runtime/auth.py +149 -0
  141. databricks_agentkit/runtime/durability/__init__.py +1 -0
  142. databricks_agentkit/runtime/durability/execution.py +133 -0
  143. databricks_agentkit/runtime/durability/heartbeat.py +64 -0
  144. databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
  145. databricks_agentkit/runtime/durability/recovery.py +70 -0
  146. databricks_agentkit/runtime/durability/store.py +33 -0
  147. databricks_agentkit/runtime/execution.py +160 -0
  148. databricks_agentkit/runtime/genie.py +224 -0
  149. databricks_agentkit/runtime/mcp_auth.py +114 -0
  150. databricks_agentkit/runtime/model_services.py +90 -0
  151. databricks_agentkit/runtime/runtime.py +273 -0
  152. databricks_agentkit/runtime/session_store_client.py +120 -0
  153. databricks_agentkit/runtime/store.py +287 -0
  154. databricks_agentkit/runtime/tool_manifest.py +223 -0
  155. databricks_agentkit/runtime/tracing.py +71 -0
  156. databricks_agentkit/runtime/types.py +118 -0
  157. databricks_agentkit/runtime/workspace.py +27 -0
  158. databricks_agentkit/session_store.py +434 -0
  159. 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}")