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,657 @@
|
|
|
1
|
+
"""Typed, comment-preserving access to an Agent Bricks project's ``agent.toml``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import re
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections.abc import Mapping, Sequence
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Literal, cast
|
|
12
|
+
|
|
13
|
+
import tomlkit
|
|
14
|
+
from tomlkit import TOMLDocument
|
|
15
|
+
from tomlkit.exceptions import ParseError
|
|
16
|
+
|
|
17
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
18
|
+
from databricks_agentbricks.project_types import (
|
|
19
|
+
AgentFramework,
|
|
20
|
+
AgentServer,
|
|
21
|
+
parse_framework,
|
|
22
|
+
parse_server,
|
|
23
|
+
)
|
|
24
|
+
from databricks_agentkit.runtime import tool_manifest
|
|
25
|
+
|
|
26
|
+
# agent.toml resource-table names, read by the manifest parsing below (and by `ab dev`/`deploy`).
|
|
27
|
+
MEMORY_STORE_TABLE = "memory_store"
|
|
28
|
+
SESSION_STORE_TABLE = "session_store"
|
|
29
|
+
# The tracing binding (`ab tracing bind` / `unbind`): the `experiment_name` key under [tracing] is
|
|
30
|
+
# the bound MLflow experiment. Its presence means tracing is on; an absent binding means off. `ab
|
|
31
|
+
# init` bootstraps a default name.
|
|
32
|
+
TRACING_TABLE = "tracing"
|
|
33
|
+
EXPERIMENT_NAME_KEY = "experiment_name"
|
|
34
|
+
|
|
35
|
+
_SCHEMA_VERSION = 1
|
|
36
|
+
_SUPPORTED_SCOPE_KINDS = {"table", "volume", "workspace"}
|
|
37
|
+
_SUPPORTED_PERMISSIONS = {"read_only", "read_write"}
|
|
38
|
+
_TOOL_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _three_part_name(value: str, description: str) -> str:
|
|
42
|
+
if (
|
|
43
|
+
not isinstance(value, str)
|
|
44
|
+
or len(value.split(".")) != 3
|
|
45
|
+
or any(not part for part in value.split("."))
|
|
46
|
+
or any(character.isspace() for character in value)
|
|
47
|
+
):
|
|
48
|
+
raise AgentCliError(
|
|
49
|
+
f"Invalid {description} {value!r}.",
|
|
50
|
+
hint=f"Use a three-part name: catalog.schema.{description.replace(' ', '_')}.",
|
|
51
|
+
)
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class Scope:
|
|
57
|
+
"""One sandbox downscope resource."""
|
|
58
|
+
|
|
59
|
+
kind: str
|
|
60
|
+
value: str
|
|
61
|
+
permission: str = "read_only"
|
|
62
|
+
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
if self.kind not in _SUPPORTED_SCOPE_KINDS:
|
|
65
|
+
raise AgentCliError(
|
|
66
|
+
f"Unsupported sandbox scope kind {self.kind!r}.",
|
|
67
|
+
hint=f"Supported scope kinds: {', '.join(sorted(_SUPPORTED_SCOPE_KINDS))}.",
|
|
68
|
+
)
|
|
69
|
+
if self.permission not in _SUPPORTED_PERMISSIONS:
|
|
70
|
+
raise AgentCliError(f"Unsupported sandbox permission {self.permission!r}.")
|
|
71
|
+
if self.kind == "workspace":
|
|
72
|
+
if not self.value.startswith("/Workspace/") or any(
|
|
73
|
+
character in self.value for character in ("\r", "\n", "\t")
|
|
74
|
+
):
|
|
75
|
+
raise AgentCliError(
|
|
76
|
+
f"Invalid workspace scope {self.value!r}.",
|
|
77
|
+
hint="Workspace paths must begin with /Workspace/.",
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
_three_part_name(self.value, f"{self.kind} scope")
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def table(cls, value: str, permission: str = "read_only") -> "Scope":
|
|
84
|
+
return cls(kind="table", value=value, permission=permission)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def volume(cls, value: str, permission: str = "read_only") -> "Scope":
|
|
88
|
+
return cls(kind="volume", value=value, permission=permission)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def workspace(cls, value: str, permission: str = "read_only") -> "Scope":
|
|
92
|
+
return cls(kind="workspace", value=value, permission=permission)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def parse(cls, value: str, permission: str = "read_only") -> "Scope":
|
|
96
|
+
"""Parse ``kind:value`` CLI form, defaulting dotted names to volumes."""
|
|
97
|
+
original = value.strip()
|
|
98
|
+
if not original:
|
|
99
|
+
raise AgentCliError("Sandbox scopes cannot be empty.")
|
|
100
|
+
prefix, separator, remainder = original.partition(":")
|
|
101
|
+
if separator and prefix in _SUPPORTED_SCOPE_KINDS:
|
|
102
|
+
return cls(prefix, remainder.strip(), permission)
|
|
103
|
+
if original.startswith("/Workspace/"):
|
|
104
|
+
return cls.workspace(original, permission)
|
|
105
|
+
return cls.volume(original, permission)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def resource(self) -> str:
|
|
109
|
+
return f"{self.kind}:{self.value}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class ToolSource:
|
|
114
|
+
"""Discriminated source for one tool binding."""
|
|
115
|
+
|
|
116
|
+
kind: str
|
|
117
|
+
service: str | None = None
|
|
118
|
+
function: str | None = None
|
|
119
|
+
space_id: str | None = None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class ToolPolicy:
|
|
124
|
+
"""Protected runtime policy for a tool binding."""
|
|
125
|
+
|
|
126
|
+
downscope: tuple[Scope, ...] = ()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class ToolSpec:
|
|
131
|
+
"""One framework-neutral tool binding from ``agent.toml``."""
|
|
132
|
+
|
|
133
|
+
id: str
|
|
134
|
+
source: ToolSource
|
|
135
|
+
policy: ToolPolicy = field(default_factory=ToolPolicy)
|
|
136
|
+
auth: Literal["user", "app"] | None = None
|
|
137
|
+
|
|
138
|
+
def __post_init__(self) -> None:
|
|
139
|
+
source_values = {"kind": self.source.kind}
|
|
140
|
+
for key in ("service", "function", "space_id"):
|
|
141
|
+
value = getattr(self.source, key)
|
|
142
|
+
if value is not None:
|
|
143
|
+
source_values[key] = value
|
|
144
|
+
try:
|
|
145
|
+
tool_manifest.validate_genie_source(
|
|
146
|
+
self.id, source_values, has_downscope=bool(self.policy.downscope)
|
|
147
|
+
)
|
|
148
|
+
except tool_manifest.ToolManifestError as exc:
|
|
149
|
+
raise AgentCliError(str(exc)) from exc
|
|
150
|
+
kind = self.source.kind
|
|
151
|
+
if self.auth is not None and self.auth not in ("user", "app"):
|
|
152
|
+
raise AgentCliError("Tool auth must be 'user' or 'app'.")
|
|
153
|
+
if kind in {"genie_one", "genie_agent"}:
|
|
154
|
+
return
|
|
155
|
+
if not _TOOL_ID.fullmatch(self.id):
|
|
156
|
+
raise AgentCliError(f"Invalid tool id {self.id!r}.")
|
|
157
|
+
if kind == "uc_function" and self.auth == "user":
|
|
158
|
+
raise AgentCliError("UC function auth supports only app/default identity.")
|
|
159
|
+
if kind == "sandbox":
|
|
160
|
+
if self.source.service != "system.ai.sandbox":
|
|
161
|
+
raise AgentCliError("Sandbox tools must bind system.ai.sandbox.")
|
|
162
|
+
if not self.policy.downscope:
|
|
163
|
+
raise AgentCliError("Sandbox tools require at least one scope.")
|
|
164
|
+
elif kind == "mcp":
|
|
165
|
+
if self.source.service is None:
|
|
166
|
+
raise AgentCliError("MCP tools require a service.")
|
|
167
|
+
_three_part_name(self.source.service, "MCP service")
|
|
168
|
+
if self.policy.downscope:
|
|
169
|
+
raise AgentCliError("Generic MCP tools do not accept sandbox scopes.")
|
|
170
|
+
elif kind == "uc_function":
|
|
171
|
+
if self.source.function is None:
|
|
172
|
+
raise AgentCliError("UC function tools require a function name.")
|
|
173
|
+
_three_part_name(self.source.function, "UC function")
|
|
174
|
+
if self.policy.downscope:
|
|
175
|
+
raise AgentCliError("UC function tools do not accept sandbox scopes.")
|
|
176
|
+
elif kind == "python":
|
|
177
|
+
raise AgentCliError(
|
|
178
|
+
"Python tools are code-first and cannot be declared in agent.toml.",
|
|
179
|
+
hint="Remove this entry and wire the tool through framework-native agent code.",
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
raise AgentCliError(f"Unsupported tool source kind {kind!r}.")
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def sandbox(
|
|
186
|
+
cls,
|
|
187
|
+
tool_id: str,
|
|
188
|
+
*,
|
|
189
|
+
scopes: Sequence[Scope],
|
|
190
|
+
auth: Literal["user", "app"] | None = None,
|
|
191
|
+
) -> "ToolSpec":
|
|
192
|
+
return cls(
|
|
193
|
+
id=tool_id,
|
|
194
|
+
source=ToolSource(kind="sandbox", service="system.ai.sandbox"),
|
|
195
|
+
policy=ToolPolicy(tuple(scopes)),
|
|
196
|
+
auth=auth,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def mcp(
|
|
201
|
+
cls, tool_id: str, *, service: str, auth: Literal["user", "app"] | None = None
|
|
202
|
+
) -> "ToolSpec":
|
|
203
|
+
return cls(id=tool_id, source=ToolSource(kind="mcp", service=service), auth=auth)
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def uc_function(
|
|
207
|
+
cls, tool_id: str, *, function: str, auth: Literal["app"] | None = None
|
|
208
|
+
) -> "ToolSpec":
|
|
209
|
+
return cls(
|
|
210
|
+
id=tool_id,
|
|
211
|
+
source=ToolSource(kind="uc_function", function=function),
|
|
212
|
+
auth=auth,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def genie_one(
|
|
217
|
+
cls,
|
|
218
|
+
tool_id: str = "genie_one",
|
|
219
|
+
*,
|
|
220
|
+
auth: Literal["user", "app"] | None = None,
|
|
221
|
+
) -> "ToolSpec":
|
|
222
|
+
return cls(id=tool_id, source=ToolSource(kind="genie_one"), auth=auth)
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def genie_agent(
|
|
226
|
+
cls,
|
|
227
|
+
tool_id: str,
|
|
228
|
+
*,
|
|
229
|
+
space_id: str,
|
|
230
|
+
auth: Literal["user", "app"] | None = None,
|
|
231
|
+
) -> "ToolSpec":
|
|
232
|
+
return cls(
|
|
233
|
+
id=tool_id,
|
|
234
|
+
source=ToolSource(kind="genie_agent", space_id=space_id),
|
|
235
|
+
auth=auth,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _required_string(value: object, description: str) -> str:
|
|
240
|
+
if not isinstance(value, str) or not value:
|
|
241
|
+
raise AgentCliError(f"agent.toml must declare {description}.")
|
|
242
|
+
return value
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def default_store_name(project_name: str, suffix: str, token: str | None = None) -> str:
|
|
246
|
+
"""A store display name derived from the project directory, e.g. ``my-agent`` -> ``my-agent-memory``.
|
|
247
|
+
|
|
248
|
+
Sanitized to the store display-name charset (lower-case alphanumerics and hyphens); a name that
|
|
249
|
+
reduces to nothing (e.g. only punctuation) falls back to ``agent``. An optional per-scaffold
|
|
250
|
+
``token`` is inserted before the suffix (``my-agent-<token>-memory``) so fresh scaffolds get
|
|
251
|
+
distinct stores while the name still ends with the store kind.
|
|
252
|
+
"""
|
|
253
|
+
slug = re.sub(r"[^a-z0-9-]+", "-", project_name.lower()).strip("-")
|
|
254
|
+
middle = f"{token}-" if token else ""
|
|
255
|
+
return f"{slug or 'agent'}-{middle}{suffix}"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _store_name_from_manifest(value: object, table: str) -> str | None:
|
|
259
|
+
"""Read the ``name`` from a ``[memory_store]`` / ``[session_store]`` table, or None if absent."""
|
|
260
|
+
if value is None:
|
|
261
|
+
return None
|
|
262
|
+
if not isinstance(value, Mapping):
|
|
263
|
+
raise AgentCliError(f"agent.toml [{table}] must be a table.")
|
|
264
|
+
return _required_string(cast(Mapping[str, Any], value).get("name"), f"[{table}] name")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _store_id_from_manifest(value: object) -> str | None:
|
|
268
|
+
"""Read the optional bare store ``id`` from a ``[memory_store]`` table, or None if absent."""
|
|
269
|
+
if not isinstance(value, Mapping):
|
|
270
|
+
return None
|
|
271
|
+
store_id = cast(Mapping[str, Any], value).get("id")
|
|
272
|
+
return store_id if isinstance(store_id, str) and store_id else None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _scope_from_manifest(value: object) -> Scope:
|
|
276
|
+
if not isinstance(value, Mapping):
|
|
277
|
+
raise AgentCliError("Sandbox downscope entries must be TOML tables.")
|
|
278
|
+
value = cast(Mapping[str, Any], value)
|
|
279
|
+
resource = _required_string(value.get("resource"), "a downscope resource")
|
|
280
|
+
permission = value.get("permission", "read_only")
|
|
281
|
+
if not isinstance(permission, str):
|
|
282
|
+
raise AgentCliError("Sandbox downscope permission must be a string.")
|
|
283
|
+
prefix, separator, name = resource.partition(":")
|
|
284
|
+
if not separator:
|
|
285
|
+
raise AgentCliError(
|
|
286
|
+
f"Invalid sandbox downscope resource {resource!r}.",
|
|
287
|
+
hint="Use table:<name>, volume:<name>, or workspace:<path>.",
|
|
288
|
+
)
|
|
289
|
+
return Scope(prefix, name, permission)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _tool_from_manifest(value: object) -> ToolSpec:
|
|
293
|
+
if not isinstance(value, Mapping):
|
|
294
|
+
raise AgentCliError("Each agent.toml tool must be a TOML table.")
|
|
295
|
+
value = cast(Mapping[str, Any], value)
|
|
296
|
+
source = value.get("source")
|
|
297
|
+
if not isinstance(source, Mapping):
|
|
298
|
+
raise AgentCliError("Each agent.toml tool must declare a source table.")
|
|
299
|
+
source = cast(Mapping[str, Any], source)
|
|
300
|
+
kind = _required_string(source.get("kind"), "a source kind")
|
|
301
|
+
policy_value = value.get("policy", {})
|
|
302
|
+
if not isinstance(policy_value, Mapping):
|
|
303
|
+
raise AgentCliError("Tool policy must be a TOML table.")
|
|
304
|
+
policy_value = cast(Mapping[str, Any], policy_value)
|
|
305
|
+
tool_id = _required_string(value.get("id"), "an id")
|
|
306
|
+
try:
|
|
307
|
+
tool_manifest.validate_genie_source(
|
|
308
|
+
tool_id, source, has_downscope="downscope" in policy_value
|
|
309
|
+
)
|
|
310
|
+
except tool_manifest.ToolManifestError as exc:
|
|
311
|
+
raise AgentCliError(str(exc)) from exc
|
|
312
|
+
downscope_value = policy_value.get("downscope", [])
|
|
313
|
+
if not isinstance(downscope_value, list):
|
|
314
|
+
raise AgentCliError("Tool policy downscope must be an array.")
|
|
315
|
+
return ToolSpec(
|
|
316
|
+
id=tool_id,
|
|
317
|
+
source=ToolSource(
|
|
318
|
+
kind=kind,
|
|
319
|
+
service=source.get("service") if isinstance(source.get("service"), str) else None,
|
|
320
|
+
function=source.get("function") if isinstance(source.get("function"), str) else None,
|
|
321
|
+
space_id=source.get("space_id") if isinstance(source.get("space_id"), str) else None,
|
|
322
|
+
),
|
|
323
|
+
policy=ToolPolicy(tuple(_scope_from_manifest(item) for item in downscope_value)),
|
|
324
|
+
auth=value.get("auth"),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _inline_table(values: Mapping[str, str]) -> Any:
|
|
329
|
+
table = tomlkit.inline_table()
|
|
330
|
+
for key, value in values.items():
|
|
331
|
+
table[key] = value
|
|
332
|
+
return table
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _tool_table(spec: ToolSpec) -> Any:
|
|
336
|
+
table = tomlkit.table()
|
|
337
|
+
table.add("id", spec.id)
|
|
338
|
+
if spec.auth is not None:
|
|
339
|
+
table.add("auth", spec.auth)
|
|
340
|
+
source_values = {"kind": spec.source.kind}
|
|
341
|
+
for key in ("service", "function", "space_id"):
|
|
342
|
+
value = getattr(spec.source, key)
|
|
343
|
+
if value is not None:
|
|
344
|
+
source_values[key] = value
|
|
345
|
+
table.add("source", _inline_table(source_values))
|
|
346
|
+
if spec.policy.downscope:
|
|
347
|
+
downscope = tomlkit.array()
|
|
348
|
+
for scope in spec.policy.downscope:
|
|
349
|
+
downscope.append(
|
|
350
|
+
_inline_table({"resource": scope.resource, "permission": scope.permission})
|
|
351
|
+
)
|
|
352
|
+
policy = tomlkit.inline_table()
|
|
353
|
+
policy["downscope"] = downscope
|
|
354
|
+
table.add("policy", policy)
|
|
355
|
+
return table
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class AgentProject:
|
|
359
|
+
"""Loaded mutable view of a project's canonical agent manifest."""
|
|
360
|
+
|
|
361
|
+
def __init__(
|
|
362
|
+
self,
|
|
363
|
+
root: pathlib.Path,
|
|
364
|
+
document: TOMLDocument,
|
|
365
|
+
framework: AgentFramework,
|
|
366
|
+
server: AgentServer,
|
|
367
|
+
tools: list[ToolSpec],
|
|
368
|
+
memory_store: str | None = None,
|
|
369
|
+
session_store: str | None = None,
|
|
370
|
+
memory_store_id: str | None = None,
|
|
371
|
+
deployment_name: str | None = None,
|
|
372
|
+
trace_experiment_name: str | None = None,
|
|
373
|
+
) -> None:
|
|
374
|
+
self.root = root
|
|
375
|
+
self.path = root / "agent.toml"
|
|
376
|
+
self._document = document
|
|
377
|
+
self.framework = framework
|
|
378
|
+
# Server selection is deployment behavior, so agent.toml—not hidden template metadata—is
|
|
379
|
+
# the source of truth for whether Agent Bricks provisions and wires a Runtime Store.
|
|
380
|
+
self.server = server
|
|
381
|
+
self.tools = tools
|
|
382
|
+
# Managed store bindings declared in agent.toml; None = unbound. memory_store_id is the bare
|
|
383
|
+
# store id the runtime needs for the entries API (the display name can't be used there).
|
|
384
|
+
self.memory_store = memory_store
|
|
385
|
+
self.session_store = session_store
|
|
386
|
+
self.memory_store_id = memory_store_id
|
|
387
|
+
# Bare deployment names get the `agent-bricks-` prefix.
|
|
388
|
+
self.deployment_name = deployment_name
|
|
389
|
+
# Tracing config: the MLflow experiment NAME to trace to (a workspace path). Its presence IS
|
|
390
|
+
# the enable switch: a bound name means tracing is on (deploy get-or-creates it); None means
|
|
391
|
+
# unbound, i.e. off. Storing a name (not an id) keeps the binding valid across workspaces and
|
|
392
|
+
# profiles, since an id is workspace-local. `ab init` bootstraps a default name.
|
|
393
|
+
self.trace_experiment_name = trace_experiment_name
|
|
394
|
+
|
|
395
|
+
@classmethod
|
|
396
|
+
def load(cls, root: pathlib.Path | str | None = None) -> "AgentProject":
|
|
397
|
+
try:
|
|
398
|
+
project_root = (
|
|
399
|
+
tool_manifest.project_root()
|
|
400
|
+
if root is None
|
|
401
|
+
else pathlib.Path(root).expanduser().resolve()
|
|
402
|
+
)
|
|
403
|
+
except RuntimeError as exc:
|
|
404
|
+
raise AgentCliError(str(exc)) from exc
|
|
405
|
+
path = project_root / "agent.toml"
|
|
406
|
+
try:
|
|
407
|
+
document = tomlkit.parse(path.read_text(encoding="utf-8"))
|
|
408
|
+
except FileNotFoundError as exc:
|
|
409
|
+
raise AgentCliError(
|
|
410
|
+
f"Could not find agent.toml in {project_root}.",
|
|
411
|
+
hint="This command needs an Agent Bricks project. Run `ab init` to create one, "
|
|
412
|
+
"or point at an existing project with --source <dir>.",
|
|
413
|
+
) from exc
|
|
414
|
+
except (OSError, ParseError) as exc:
|
|
415
|
+
raise AgentCliError(f"Could not read agent manifest at {path}: {exc}.") from exc
|
|
416
|
+
if document.get("schema_version") != _SCHEMA_VERSION:
|
|
417
|
+
raise AgentCliError(
|
|
418
|
+
f"Unsupported agent manifest schema in {path}.",
|
|
419
|
+
hint=f"Expected schema_version = {_SCHEMA_VERSION}.",
|
|
420
|
+
)
|
|
421
|
+
agent = document.get("agent")
|
|
422
|
+
if not isinstance(agent, Mapping):
|
|
423
|
+
raise AgentCliError("agent.toml must declare an [agent] table.")
|
|
424
|
+
framework = parse_framework(_required_string(agent.get("framework"), "agent.framework"))
|
|
425
|
+
server = parse_server(_required_string(agent.get("server"), "agent.server"))
|
|
426
|
+
deployment_name = agent.get("deployment_name")
|
|
427
|
+
if deployment_name is not None and not (
|
|
428
|
+
isinstance(deployment_name, str) and deployment_name
|
|
429
|
+
):
|
|
430
|
+
raise AgentCliError("agent.toml [agent] deployment_name must be a non-empty string.")
|
|
431
|
+
raw_tools = document.get("tools", [])
|
|
432
|
+
if not isinstance(raw_tools, list):
|
|
433
|
+
raise AgentCliError("agent.toml tools must be an array of tables.")
|
|
434
|
+
tools = [_tool_from_manifest(item) for item in raw_tools]
|
|
435
|
+
ids = [tool.id for tool in tools]
|
|
436
|
+
if len(ids) != len(set(ids)):
|
|
437
|
+
raise AgentCliError("agent.toml tool ids must be unique.")
|
|
438
|
+
memory_store = _store_name_from_manifest(
|
|
439
|
+
document.get(MEMORY_STORE_TABLE), MEMORY_STORE_TABLE
|
|
440
|
+
)
|
|
441
|
+
memory_store_id = _store_id_from_manifest(document.get(MEMORY_STORE_TABLE))
|
|
442
|
+
session_store = _store_name_from_manifest(
|
|
443
|
+
document.get(SESSION_STORE_TABLE), SESSION_STORE_TABLE
|
|
444
|
+
)
|
|
445
|
+
tracing_table = document.get(TRACING_TABLE)
|
|
446
|
+
trace_experiment_name: str | None = None
|
|
447
|
+
if isinstance(tracing_table, Mapping):
|
|
448
|
+
raw_experiment = tracing_table.get(EXPERIMENT_NAME_KEY)
|
|
449
|
+
trace_experiment_name = (
|
|
450
|
+
str(raw_experiment) if isinstance(raw_experiment, str) and raw_experiment else None
|
|
451
|
+
)
|
|
452
|
+
return cls(
|
|
453
|
+
project_root,
|
|
454
|
+
document,
|
|
455
|
+
framework,
|
|
456
|
+
server,
|
|
457
|
+
tools,
|
|
458
|
+
memory_store,
|
|
459
|
+
session_store,
|
|
460
|
+
memory_store_id,
|
|
461
|
+
str(deployment_name) if deployment_name is not None else None,
|
|
462
|
+
trace_experiment_name,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
@classmethod
|
|
466
|
+
def create(
|
|
467
|
+
cls,
|
|
468
|
+
root: pathlib.Path | str,
|
|
469
|
+
*,
|
|
470
|
+
framework: str,
|
|
471
|
+
server: str,
|
|
472
|
+
memory_store: str | None = None,
|
|
473
|
+
session_store: str | None = None,
|
|
474
|
+
experiment_name: str | None = None,
|
|
475
|
+
) -> "AgentProject":
|
|
476
|
+
selected_framework = parse_framework(framework)
|
|
477
|
+
selected_server = parse_server(server)
|
|
478
|
+
project_root = pathlib.Path(root).expanduser().resolve()
|
|
479
|
+
document = tomlkit.document()
|
|
480
|
+
document.add("schema_version", _SCHEMA_VERSION)
|
|
481
|
+
document.add(tomlkit.nl())
|
|
482
|
+
agent = tomlkit.table()
|
|
483
|
+
agent.add("framework", selected_framework.value)
|
|
484
|
+
agent.add("server", selected_server.value)
|
|
485
|
+
document.add("agent", agent)
|
|
486
|
+
project = cls(
|
|
487
|
+
project_root,
|
|
488
|
+
document,
|
|
489
|
+
selected_framework,
|
|
490
|
+
selected_server,
|
|
491
|
+
[],
|
|
492
|
+
)
|
|
493
|
+
if memory_store:
|
|
494
|
+
project.bind_memory_store(memory_store)
|
|
495
|
+
if session_store:
|
|
496
|
+
project.bind_session_store(session_store)
|
|
497
|
+
if experiment_name:
|
|
498
|
+
project.bind_tracing(experiment_name)
|
|
499
|
+
return project
|
|
500
|
+
|
|
501
|
+
def set_deployment_name(self, name: str) -> bool:
|
|
502
|
+
"""Record the deployment's base name under [agent].deployment_name. True if it changed."""
|
|
503
|
+
name = _required_string(name, "[agent] deployment_name")
|
|
504
|
+
if self.deployment_name == name:
|
|
505
|
+
return False
|
|
506
|
+
agent = self._document.get("agent")
|
|
507
|
+
if not isinstance(agent, Mapping):
|
|
508
|
+
raise AgentCliError("agent.toml must declare an [agent] table.")
|
|
509
|
+
agent["deployment_name"] = name
|
|
510
|
+
self.deployment_name = name
|
|
511
|
+
return True
|
|
512
|
+
|
|
513
|
+
def add_tool(self, spec: ToolSpec) -> bool:
|
|
514
|
+
for existing in self.tools:
|
|
515
|
+
if existing.id != spec.id:
|
|
516
|
+
continue
|
|
517
|
+
if existing == spec:
|
|
518
|
+
return False
|
|
519
|
+
|
|
520
|
+
def _summary(s: ToolSpec) -> str:
|
|
521
|
+
src = s.source
|
|
522
|
+
return src.service or src.function or src.space_id or src.kind
|
|
523
|
+
|
|
524
|
+
raise AgentCliError(
|
|
525
|
+
f"Tool id {spec.id!r} already exists with a different configuration "
|
|
526
|
+
f"(existing: {_summary(existing)}; requested: {_summary(spec)}).",
|
|
527
|
+
hint="Use --name to add it under a different id, or remove the existing "
|
|
528
|
+
"tool from agent.toml first.",
|
|
529
|
+
)
|
|
530
|
+
raw_tools = self._document.get("tools")
|
|
531
|
+
if raw_tools is None:
|
|
532
|
+
raw_tools = tomlkit.aot()
|
|
533
|
+
self._document.append("tools", raw_tools)
|
|
534
|
+
elif not hasattr(raw_tools, "append"):
|
|
535
|
+
raise AgentCliError("agent.toml tools must be an array of tables.")
|
|
536
|
+
raw_tools.append(_tool_table(spec))
|
|
537
|
+
self.tools.append(spec)
|
|
538
|
+
return True
|
|
539
|
+
|
|
540
|
+
def remove_tool(self, tool_id: str) -> bool:
|
|
541
|
+
index = next((index for index, tool in enumerate(self.tools) if tool.id == tool_id), None)
|
|
542
|
+
if index is None:
|
|
543
|
+
return False
|
|
544
|
+
raw_tools = self._document.get("tools")
|
|
545
|
+
if not isinstance(raw_tools, list):
|
|
546
|
+
raise AgentCliError("agent.toml tools must be an array of tables.")
|
|
547
|
+
del raw_tools[index]
|
|
548
|
+
del self.tools[index]
|
|
549
|
+
return True
|
|
550
|
+
|
|
551
|
+
def bind_memory_store(self, name: str, store_id: str | None = None) -> bool:
|
|
552
|
+
"""Declare the memory store binding in agent.toml. Returns True if it changed.
|
|
553
|
+
|
|
554
|
+
``store_id`` is the bare store id (``memory-stores/<id>`` minus the prefix). `ab deploy`
|
|
555
|
+
resolves the id fresh and injects it via ``AGENT_MEMORY_STORE``; the field is only recorded for
|
|
556
|
+
legacy/hand-written bindings that pin the id in the manifest.
|
|
557
|
+
"""
|
|
558
|
+
return self._set_store(MEMORY_STORE_TABLE, name, store_id)
|
|
559
|
+
|
|
560
|
+
def bind_session_store(self, name: str) -> bool:
|
|
561
|
+
"""Declare the session store binding in agent.toml. Returns True if it changed."""
|
|
562
|
+
return self._set_store(SESSION_STORE_TABLE, name)
|
|
563
|
+
|
|
564
|
+
def unbind_memory_store(self) -> bool:
|
|
565
|
+
"""Remove the memory store binding from agent.toml. Returns True if it was present."""
|
|
566
|
+
return self._clear_store(MEMORY_STORE_TABLE)
|
|
567
|
+
|
|
568
|
+
def unbind_session_store(self) -> bool:
|
|
569
|
+
"""Remove the session store binding from agent.toml. Returns True if it was present."""
|
|
570
|
+
return self._clear_store(SESSION_STORE_TABLE)
|
|
571
|
+
|
|
572
|
+
def bind_tracing(self, experiment_name: str) -> bool:
|
|
573
|
+
"""Bind tracing to an MLflow experiment NAME (an experiment path). Returns True if changed.
|
|
574
|
+
|
|
575
|
+
The binding's presence is the enable switch: a bound name means tracing is on (deploy
|
|
576
|
+
get-or-creates it). Storing a name (not an id) keeps the binding portable across
|
|
577
|
+
workspaces/profiles.
|
|
578
|
+
"""
|
|
579
|
+
experiment_name = _required_string(
|
|
580
|
+
experiment_name, f"[{TRACING_TABLE}] {EXPERIMENT_NAME_KEY}"
|
|
581
|
+
)
|
|
582
|
+
if self.trace_experiment_name == experiment_name:
|
|
583
|
+
return False
|
|
584
|
+
table = self._document.get(TRACING_TABLE)
|
|
585
|
+
if not isinstance(table, Mapping):
|
|
586
|
+
table = tomlkit.table()
|
|
587
|
+
self._document.append(TRACING_TABLE, table)
|
|
588
|
+
table[EXPERIMENT_NAME_KEY] = experiment_name
|
|
589
|
+
self.trace_experiment_name = experiment_name
|
|
590
|
+
return True
|
|
591
|
+
|
|
592
|
+
def unbind_tracing(self) -> bool:
|
|
593
|
+
"""Unbind tracing: remove the experiment binding so tracing is off. Returns True if changed."""
|
|
594
|
+
if self.trace_experiment_name is None:
|
|
595
|
+
return False
|
|
596
|
+
table = self._document.get(TRACING_TABLE)
|
|
597
|
+
if isinstance(table, Mapping) and EXPERIMENT_NAME_KEY in table:
|
|
598
|
+
del table[EXPERIMENT_NAME_KEY]
|
|
599
|
+
if isinstance(table, Mapping) and not table:
|
|
600
|
+
del self._document[TRACING_TABLE]
|
|
601
|
+
self.trace_experiment_name = None
|
|
602
|
+
return True
|
|
603
|
+
|
|
604
|
+
def _set_store(self, table: str, name: str, store_id: str | None = None) -> bool:
|
|
605
|
+
name = _required_string(name, f"[{table}] name")
|
|
606
|
+
if getattr(self, table) == name and getattr(self, f"{table}_id", None) == store_id:
|
|
607
|
+
return False
|
|
608
|
+
existing = self._document.get(table)
|
|
609
|
+
if isinstance(existing, Mapping):
|
|
610
|
+
existing["name"] = name
|
|
611
|
+
if store_id:
|
|
612
|
+
existing["id"] = store_id
|
|
613
|
+
elif "id" in existing:
|
|
614
|
+
del existing["id"]
|
|
615
|
+
else:
|
|
616
|
+
store_table = tomlkit.table()
|
|
617
|
+
store_table.add("name", name)
|
|
618
|
+
if store_id:
|
|
619
|
+
store_table.add("id", store_id)
|
|
620
|
+
self._document.append(table, store_table)
|
|
621
|
+
setattr(self, table, name)
|
|
622
|
+
if hasattr(self, f"{table}_id"):
|
|
623
|
+
setattr(self, f"{table}_id", store_id)
|
|
624
|
+
return True
|
|
625
|
+
|
|
626
|
+
def _clear_store(self, table: str) -> bool:
|
|
627
|
+
if getattr(self, table) is None:
|
|
628
|
+
return False
|
|
629
|
+
if table in self._document:
|
|
630
|
+
del self._document[table]
|
|
631
|
+
setattr(self, table, None)
|
|
632
|
+
if hasattr(self, f"{table}_id"):
|
|
633
|
+
setattr(self, f"{table}_id", None)
|
|
634
|
+
return True
|
|
635
|
+
|
|
636
|
+
def write(self) -> pathlib.Path:
|
|
637
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
638
|
+
temporary: pathlib.Path | None = None
|
|
639
|
+
try:
|
|
640
|
+
with tempfile.NamedTemporaryFile(
|
|
641
|
+
mode="w",
|
|
642
|
+
encoding="utf-8",
|
|
643
|
+
dir=self.path.parent,
|
|
644
|
+
prefix=f".{self.path.name}.",
|
|
645
|
+
suffix=".tmp",
|
|
646
|
+
delete=False,
|
|
647
|
+
) as output:
|
|
648
|
+
temporary = pathlib.Path(output.name)
|
|
649
|
+
output.write(tomlkit.dumps(self._document))
|
|
650
|
+
output.flush()
|
|
651
|
+
os.fsync(output.fileno())
|
|
652
|
+
os.replace(temporary, self.path)
|
|
653
|
+
except OSError as exc:
|
|
654
|
+
if temporary is not None:
|
|
655
|
+
temporary.unlink(missing_ok=True)
|
|
656
|
+
raise AgentCliError(f"Could not write agent manifest at {self.path}: {exc}.") from exc
|
|
657
|
+
return self.path
|