mobileops-managed-agent 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {mobileops_managed_agent-0.3.0/src/mobileops_managed_agent.egg-info → mobileops_managed_agent-0.3.2}/PKG-INFO +1 -1
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/pyproject.toml +1 -1
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/__init__.py +21 -0
- mobileops_managed_agent-0.3.2/src/mobileops_managed_agent/artifacts.py +134 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/assistant_model.py +13 -1
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/assistants.py +11 -1
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/capabilities.py +2 -2
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/client.py +11 -1
- mobileops_managed_agent-0.3.2/src/mobileops_managed_agent/model_retry.py +64 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/resources.py +17 -3
- mobileops_managed_agent-0.3.2/src/mobileops_managed_agent/tool_permissions.py +97 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2/src/mobileops_managed_agent.egg-info}/PKG-INFO +1 -1
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent.egg-info/SOURCES.txt +3 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/MANIFEST.in +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/README.md +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/setup.cfg +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/auth.py +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/credential_vault.py +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/files.py +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/response_schema.py +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/sandbox_profile.py +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/sandbox_profiles.py +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/vaults.py +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent.egg-info/dependency_links.txt +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent.egg-info/requires.txt +0 -0
- {mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mobileops-managed-agent
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Server-side SDK for the MobileOPS Managed Agent API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/InnoMindX/deepagent/tree/main/managed-agent-sdk/python
|
|
6
6
|
Project-URL: Repository, https://github.com/InnoMindX/deepagent
|
|
@@ -65,6 +65,14 @@ from .vaults import (
|
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
__all__ = [
|
|
68
|
+
"ModelRetryEvent",
|
|
69
|
+
"ModelRetryTracker",
|
|
70
|
+
"MobileOpsArtifactsClient",
|
|
71
|
+
"MobileOpsStorageClient",
|
|
72
|
+
"Artifact",
|
|
73
|
+
"ArtifactFile",
|
|
74
|
+
"ArtifactJob",
|
|
75
|
+
"StorageUsage",
|
|
68
76
|
"MobileOpsManagedAgentClient",
|
|
69
77
|
"MobileOpsAgentClient",
|
|
70
78
|
"MobileOpsFilesClient",
|
|
@@ -114,3 +122,16 @@ __all__ = [
|
|
|
114
122
|
"CredentialVaultReference",
|
|
115
123
|
"validate_credential_vault",
|
|
116
124
|
]
|
|
125
|
+
|
|
126
|
+
from .tool_permissions import validate_tool_permissions, TOOL_PERMISSIONS_CONFIG_KEY
|
|
127
|
+
|
|
128
|
+
from .artifacts import (
|
|
129
|
+
MobileOpsArtifactsClient,
|
|
130
|
+
MobileOpsStorageClient,
|
|
131
|
+
Artifact,
|
|
132
|
+
ArtifactFile,
|
|
133
|
+
ArtifactJob,
|
|
134
|
+
StorageUsage,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
from .model_retry import ModelRetryEvent, ModelRetryTracker
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Persistent deliverables, independent of the sandbox and thread lifetime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from typing import Any, TypedDict, Literal
|
|
6
|
+
from urllib.parse import quote, urlencode
|
|
7
|
+
from .resources import MobileOpsResourcesClient, MobileOpsResourcesError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ArtifactFile(TypedDict):
|
|
11
|
+
path: str
|
|
12
|
+
size: int
|
|
13
|
+
sha256: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Artifact(TypedDict):
|
|
17
|
+
id: str
|
|
18
|
+
thread_id: str
|
|
19
|
+
run_id: str
|
|
20
|
+
fingerprint: str
|
|
21
|
+
size_bytes: int
|
|
22
|
+
files: list[ArtifactFile]
|
|
23
|
+
created_at: str
|
|
24
|
+
expires_at: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ArtifactJob(TypedDict):
|
|
28
|
+
id: str
|
|
29
|
+
thread_id: str
|
|
30
|
+
run_id: str
|
|
31
|
+
status: Literal["pending", "downloading", "saved", "unchanged", "failed", "cancelled"]
|
|
32
|
+
artifact_id: str | None
|
|
33
|
+
error_code: str | None
|
|
34
|
+
error_message: str | None
|
|
35
|
+
error_status: int | None
|
|
36
|
+
created_at: str
|
|
37
|
+
expires_at: str
|
|
38
|
+
finished_at: str | None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MobileOpsArtifactJobsClient:
|
|
42
|
+
def __init__(self, transport: MobileOpsResourcesClient, base: str):
|
|
43
|
+
self._transport = transport
|
|
44
|
+
self._base = base
|
|
45
|
+
|
|
46
|
+
async def list(self, *, thread_id: str | None = None, run_id: str | None = None, limit: int = 50, offset: int = 0) -> list[ArtifactJob]:
|
|
47
|
+
query = {"limit": str(limit), "offset": str(offset)}
|
|
48
|
+
if thread_id is not None:
|
|
49
|
+
query["thread_id"] = thread_id
|
|
50
|
+
if run_id is not None:
|
|
51
|
+
query["run_id"] = run_id
|
|
52
|
+
return (await self._transport._request_json("GET", self._base + "?" + urlencode(query)))["jobs"]
|
|
53
|
+
|
|
54
|
+
async def retrieve(self, job_id: str) -> ArtifactJob:
|
|
55
|
+
return (await self._transport._request_json("GET", self._base + "/" + quote(job_id, safe="")))["job"]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StorageUsage(TypedDict):
|
|
59
|
+
used_bytes: int
|
|
60
|
+
reserved_bytes: int
|
|
61
|
+
limit_bytes: int | None
|
|
62
|
+
default_limit_bytes: int
|
|
63
|
+
effective_limit_bytes: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MobileOpsArtifactsClient:
|
|
67
|
+
def __init__(self, **options: Any):
|
|
68
|
+
self._transport = MobileOpsResourcesClient(**options)
|
|
69
|
+
self._base = options["api_url"].rstrip("/") + "/context-hub/v1/artifacts"
|
|
70
|
+
self.jobs = MobileOpsArtifactJobsClient(self._transport, options["api_url"].rstrip("/") + "/context-hub/v1/artifact-jobs")
|
|
71
|
+
|
|
72
|
+
async def list(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
thread_id: str | None = None,
|
|
76
|
+
run_id: str | None = None,
|
|
77
|
+
limit: int = 50,
|
|
78
|
+
offset: int = 0,
|
|
79
|
+
) -> list[Artifact]:
|
|
80
|
+
query = {"limit": str(limit), "offset": str(offset)}
|
|
81
|
+
if thread_id is not None:
|
|
82
|
+
query["thread_id"] = thread_id
|
|
83
|
+
if run_id is not None:
|
|
84
|
+
query["run_id"] = run_id
|
|
85
|
+
return (
|
|
86
|
+
await self._transport._request_json(
|
|
87
|
+
"GET", self._base + "?" + urlencode(query)
|
|
88
|
+
)
|
|
89
|
+
)["artifacts"]
|
|
90
|
+
|
|
91
|
+
async def retrieve(self, artifact_id: str) -> Artifact:
|
|
92
|
+
return (
|
|
93
|
+
await self._transport._request_json(
|
|
94
|
+
"GET", self._base + "/" + quote(artifact_id, safe="")
|
|
95
|
+
)
|
|
96
|
+
)["artifact"]
|
|
97
|
+
|
|
98
|
+
async def download(self, artifact_id: str) -> AsyncIterator[bytes]:
|
|
99
|
+
"""Stream the ZIP without buffering the whole archive in the SDK."""
|
|
100
|
+
t = self._transport
|
|
101
|
+
async with t._client.stream(
|
|
102
|
+
"GET",
|
|
103
|
+
self._base + "/" + quote(artifact_id, safe="") + "/content",
|
|
104
|
+
headers=t._headers,
|
|
105
|
+
) as response:
|
|
106
|
+
if response.is_error:
|
|
107
|
+
await response.aread()
|
|
108
|
+
try:
|
|
109
|
+
body = response.json()
|
|
110
|
+
except ValueError:
|
|
111
|
+
body = response.text
|
|
112
|
+
raise MobileOpsResourcesError(response, body)
|
|
113
|
+
async for chunk in response.aiter_bytes():
|
|
114
|
+
yield chunk
|
|
115
|
+
|
|
116
|
+
async def delete(self, artifact_id: str) -> None:
|
|
117
|
+
await self._transport._request(
|
|
118
|
+
"DELETE", self._base + "/" + quote(artifact_id, safe="")
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
async def aclose(self) -> None:
|
|
122
|
+
await self._transport.aclose()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class MobileOpsStorageClient:
|
|
126
|
+
def __init__(self, **options: Any):
|
|
127
|
+
self._transport = MobileOpsResourcesClient(**options)
|
|
128
|
+
self._url = options["api_url"].rstrip("/") + "/context-hub/v1/storage/usage"
|
|
129
|
+
|
|
130
|
+
async def usage(self) -> StorageUsage:
|
|
131
|
+
return await self._transport._request_json("GET", self._url)
|
|
132
|
+
|
|
133
|
+
async def aclose(self) -> None:
|
|
134
|
+
await self._transport.aclose()
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from .tool_permissions import TOOL_PERMISSIONS_CONFIG_KEY, validate_tool_permissions
|
|
4
|
+
|
|
3
5
|
from typing import Any, NotRequired, TypedDict
|
|
4
6
|
|
|
5
7
|
from .capabilities import (
|
|
@@ -52,6 +54,7 @@ class MobileOpsAssistant(TypedDict, total=False):
|
|
|
52
54
|
mcp_servers: dict[str, CapabilityMcpServer]
|
|
53
55
|
response_schema: JsonSchema
|
|
54
56
|
sandbox_profile: str
|
|
57
|
+
tool_permissions: dict[str, Any]
|
|
55
58
|
credential_vault: str
|
|
56
59
|
raw: Any
|
|
57
60
|
created_at: str
|
|
@@ -77,7 +80,7 @@ def _map_hub(
|
|
|
77
80
|
) -> dict[str, Any]:
|
|
78
81
|
entrypoints = item.get("entrypoints")
|
|
79
82
|
if kind == "memory":
|
|
80
|
-
if not entrypoints:
|
|
83
|
+
if "entrypoints" in item and not entrypoints:
|
|
81
84
|
raise TypeError("memory attachments require a non-empty entrypoints list")
|
|
82
85
|
elif entrypoints is not None:
|
|
83
86
|
raise TypeError(
|
|
@@ -133,6 +136,7 @@ def to_wire_config(**kwargs: Any) -> MobileOpsAssistantConfig | None:
|
|
|
133
136
|
and not has_response_schema
|
|
134
137
|
and not has_sandbox_profile
|
|
135
138
|
and not has_credential_vault
|
|
139
|
+
and "tool_permissions" not in kwargs
|
|
136
140
|
):
|
|
137
141
|
return None
|
|
138
142
|
configurable: dict[str, Any] = {}
|
|
@@ -153,6 +157,10 @@ def to_wire_config(**kwargs: Any) -> MobileOpsAssistantConfig | None:
|
|
|
153
157
|
configurable[CREDENTIAL_VAULT_CONFIG_KEY] = validate_credential_vault(
|
|
154
158
|
{"ref": credential_vault}
|
|
155
159
|
)
|
|
160
|
+
if "tool_permissions" in kwargs:
|
|
161
|
+
configurable[TOOL_PERMISSIONS_CONFIG_KEY] = validate_tool_permissions(
|
|
162
|
+
kwargs["tool_permissions"], list((kwargs.get("mcp_servers") or {}).keys())
|
|
163
|
+
)
|
|
156
164
|
return {"configurable": configurable} # type: ignore[return-value]
|
|
157
165
|
|
|
158
166
|
|
|
@@ -327,6 +335,10 @@ def from_wire_assistant(raw: object) -> MobileOpsAssistant:
|
|
|
327
335
|
product["response_schema"] = response_schema
|
|
328
336
|
if sandbox_profile is not None:
|
|
329
337
|
product["sandbox_profile"] = sandbox_profile
|
|
338
|
+
if configurable and TOOL_PERMISSIONS_CONFIG_KEY in configurable:
|
|
339
|
+
product["tool_permissions"] = validate_tool_permissions(
|
|
340
|
+
configurable[TOOL_PERMISSIONS_CONFIG_KEY]
|
|
341
|
+
)
|
|
330
342
|
if credential_vault is not None:
|
|
331
343
|
product["credential_vault"] = credential_vault
|
|
332
344
|
if isinstance(doc.get("name"), str):
|
|
@@ -52,6 +52,7 @@ class MobileOpsAssistantsClient:
|
|
|
52
52
|
response_schema: JsonSchema | None = None,
|
|
53
53
|
sandbox_profile: str | None = None,
|
|
54
54
|
credential_vault: str | None = None,
|
|
55
|
+
tool_permissions: dict[str, Any] | _NotGiven = _NOT_GIVEN,
|
|
55
56
|
assistant_id: str | None = None,
|
|
56
57
|
if_exists: OnConflictBehavior | None = None,
|
|
57
58
|
context: Context | None = None,
|
|
@@ -94,6 +95,8 @@ class MobileOpsAssistantsClient:
|
|
|
94
95
|
if credential_vault is not None:
|
|
95
96
|
product_kwargs["credential_vault"] = credential_vault
|
|
96
97
|
|
|
98
|
+
if not isinstance(tool_permissions, _NotGiven):
|
|
99
|
+
product_kwargs["tool_permissions"] = tool_permissions
|
|
97
100
|
wire = to_wire_create_kwargs(**product_kwargs)
|
|
98
101
|
native_kwargs = {
|
|
99
102
|
key: wire[key]
|
|
@@ -172,6 +175,7 @@ class MobileOpsAssistantsClient:
|
|
|
172
175
|
response_schema: JsonSchema | None | _NotGiven = _NOT_GIVEN,
|
|
173
176
|
sandbox_profile: str | None | _NotGiven = _NOT_GIVEN,
|
|
174
177
|
credential_vault: str | None | _NotGiven = _NOT_GIVEN,
|
|
178
|
+
tool_permissions: dict[str, Any] | _NotGiven = _NOT_GIVEN,
|
|
175
179
|
context: Context | None = None,
|
|
176
180
|
headers: Mapping[str, str] | None = None,
|
|
177
181
|
params: QueryParamTypes | None = None,
|
|
@@ -212,7 +216,9 @@ class MobileOpsAssistantsClient:
|
|
|
212
216
|
)
|
|
213
217
|
) or not isinstance(response_schema, _NotGiven) or not isinstance(
|
|
214
218
|
sandbox_profile, _NotGiven
|
|
215
|
-
) or not isinstance(credential_vault, _NotGiven)
|
|
219
|
+
) or not isinstance(credential_vault, _NotGiven) or not isinstance(
|
|
220
|
+
tool_permissions, _NotGiven
|
|
221
|
+
)
|
|
216
222
|
if changes_config:
|
|
217
223
|
current = await self.get(
|
|
218
224
|
assistant_id,
|
|
@@ -248,6 +254,10 @@ class MobileOpsAssistantsClient:
|
|
|
248
254
|
if not isinstance(sandbox_profile, _NotGiven)
|
|
249
255
|
else current.get("sandbox_profile")
|
|
250
256
|
)
|
|
257
|
+
if not isinstance(tool_permissions, _NotGiven):
|
|
258
|
+
product_kwargs["tool_permissions"] = tool_permissions
|
|
259
|
+
elif "tool_permissions" in current:
|
|
260
|
+
product_kwargs["tool_permissions"] = current["tool_permissions"]
|
|
251
261
|
product_kwargs["credential_vault"] = (
|
|
252
262
|
credential_vault
|
|
253
263
|
if not isinstance(credential_vault, _NotGiven)
|
|
@@ -298,7 +298,7 @@ _SIZE_BUDGET_PROXY_HOST = f"{'x' * 253}:65535"
|
|
|
298
298
|
|
|
299
299
|
|
|
300
300
|
def _size_budget_upstream_host(url: object) -> str | None:
|
|
301
|
-
"""Mirror backend
|
|
301
|
+
"""Mirror backend upstream Host configuration for tunneled targets."""
|
|
302
302
|
if not isinstance(url, str) or not url.strip():
|
|
303
303
|
return None
|
|
304
304
|
from urllib.parse import urlparse
|
|
@@ -337,7 +337,7 @@ def _assert_mcp_gateway_config_launchable(servers: dict) -> None:
|
|
|
337
337
|
continue
|
|
338
338
|
# Budget processd backendTunnel + auto Host injected at sandbox setup.
|
|
339
339
|
policies: dict = {
|
|
340
|
-
"backendTunnel": {"proxy": {"host": _SIZE_BUDGET_PROXY_HOST}},
|
|
340
|
+
"backendTunnel": {"mode": "connect", "proxy": {"host": _SIZE_BUDGET_PROXY_HOST}},
|
|
341
341
|
}
|
|
342
342
|
headers = server.get("headers")
|
|
343
343
|
header_set: dict = dict(headers) if isinstance(headers, dict) else {}
|
|
@@ -11,6 +11,7 @@ from .assistants import MobileOpsAssistantsClient
|
|
|
11
11
|
from .resources import MobileOpsResourcesClient
|
|
12
12
|
from .sandbox_profiles import MobileOpsSandboxProfilesClient
|
|
13
13
|
from .vaults import MobileOpsVaultsClient
|
|
14
|
+
from .artifacts import MobileOpsArtifactsClient, MobileOpsStorageClient
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class MobileOpsAgentClient:
|
|
@@ -52,6 +53,9 @@ class MobileOpsManagedAgentClient:
|
|
|
52
53
|
headers=merged_headers,
|
|
53
54
|
)
|
|
54
55
|
)
|
|
56
|
+
artifact_options = dict(api_url=api_url, api_key=api_key, headers=headers, http_client=http_client)
|
|
57
|
+
self.artifacts = MobileOpsArtifactsClient(**artifact_options)
|
|
58
|
+
self.storage = MobileOpsStorageClient(**artifact_options)
|
|
55
59
|
self.files = MobileOpsFilesClient(
|
|
56
60
|
api_url=api_url,
|
|
57
61
|
api_key=api_key,
|
|
@@ -92,7 +96,13 @@ class MobileOpsManagedAgentClient:
|
|
|
92
96
|
try:
|
|
93
97
|
await self.vaults.aclose()
|
|
94
98
|
finally:
|
|
95
|
-
|
|
99
|
+
try:
|
|
100
|
+
await self.sandbox_profiles.aclose()
|
|
101
|
+
finally:
|
|
102
|
+
try:
|
|
103
|
+
await self.artifacts.aclose()
|
|
104
|
+
finally:
|
|
105
|
+
await self.storage.aclose()
|
|
96
106
|
|
|
97
107
|
async def __aenter__(self) -> "MobileOpsManagedAgentClient":
|
|
98
108
|
return self
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Track custom model retry events while consuming the native Run stream."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from typing import Any, Literal, TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModelRetryEvent(TypedDict):
|
|
9
|
+
type: Literal["model_retry"]
|
|
10
|
+
call_id: str
|
|
11
|
+
namespace: str
|
|
12
|
+
phase: Literal["retrying", "resumed", "succeeded", "exhausted"]
|
|
13
|
+
attempt: int
|
|
14
|
+
max_retries: int
|
|
15
|
+
failed_message_ids: list[str]
|
|
16
|
+
delay_ms: int
|
|
17
|
+
reason: str | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ModelRetryTracker:
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._discarded: set[str] = set()
|
|
23
|
+
self._active: dict[str, ModelRetryEvent] = {}
|
|
24
|
+
|
|
25
|
+
def update(self, value: Any) -> ModelRetryEvent | None:
|
|
26
|
+
if not isinstance(value, dict) or value.get("type") != "model_retry":
|
|
27
|
+
return None
|
|
28
|
+
if (
|
|
29
|
+
not isinstance(value.get("call_id"), str)
|
|
30
|
+
or not isinstance(value.get("namespace"), str)
|
|
31
|
+
or value.get("phase")
|
|
32
|
+
not in ("retrying", "resumed", "succeeded", "exhausted")
|
|
33
|
+
or any(
|
|
34
|
+
type(value.get(k)) is not int or value[k] < 0
|
|
35
|
+
for k in ("attempt", "max_retries", "delay_ms")
|
|
36
|
+
)
|
|
37
|
+
or not isinstance(value.get("failed_message_ids"), list)
|
|
38
|
+
or not all(isinstance(i, str) for i in value["failed_message_ids"])
|
|
39
|
+
or "reason" not in value
|
|
40
|
+
or not (value.get("reason") is None or isinstance(value.get("reason"), str))
|
|
41
|
+
):
|
|
42
|
+
return None
|
|
43
|
+
self._discarded.update(value["failed_message_ids"])
|
|
44
|
+
if value["phase"] in ("retrying", "resumed"):
|
|
45
|
+
self._active[value["call_id"]] = value
|
|
46
|
+
else:
|
|
47
|
+
self._active.pop(value["call_id"], None)
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def pending(self) -> list[ModelRetryEvent]:
|
|
52
|
+
return list(self._active.values())
|
|
53
|
+
|
|
54
|
+
def filter(self, messages: Sequence[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
|
|
55
|
+
return [m for m in messages if m.get("id") not in self._discarded]
|
|
56
|
+
|
|
57
|
+
def reset(self) -> None:
|
|
58
|
+
"""Start tracking a different conversation."""
|
|
59
|
+
self._discarded.clear()
|
|
60
|
+
self._active.clear()
|
|
61
|
+
|
|
62
|
+
def finish(self) -> None:
|
|
63
|
+
"""Clear status at Run end, keeping failed messages hidden."""
|
|
64
|
+
self._active.clear()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from collections.abc import Mapping
|
|
4
5
|
from typing import Any, NotRequired, TypedDict
|
|
5
6
|
from urllib.parse import quote, urlencode
|
|
@@ -16,6 +17,8 @@ class ResourceVersion(TypedDict):
|
|
|
16
17
|
ref: str
|
|
17
18
|
file_count: NotRequired[int]
|
|
18
19
|
created_at: NotRequired[str]
|
|
20
|
+
size_bytes: NotRequired[int]
|
|
21
|
+
files: NotRequired[list[ResourceFile]]
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
class ResourceSummary(TypedDict):
|
|
@@ -65,10 +68,21 @@ class MobileOpsResourceVersionsClient:
|
|
|
65
68
|
return payload["resource"]
|
|
66
69
|
|
|
67
70
|
async def list(
|
|
68
|
-
self,
|
|
71
|
+
self,
|
|
72
|
+
kind: CapabilityResourceKind,
|
|
73
|
+
slug: str,
|
|
74
|
+
*,
|
|
75
|
+
versions: list[str] | None = None,
|
|
76
|
+
include_files: bool = False,
|
|
69
77
|
) -> list[ResourceVersion]:
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
params = {"include_files": str(include_files).lower()}
|
|
79
|
+
if versions is not None:
|
|
80
|
+
params["versions"] = json.dumps(versions)
|
|
81
|
+
payload = await self._resources._request_json(
|
|
82
|
+
"GET",
|
|
83
|
+
f"{self._resources._resource_url(kind, slug)}/versions?{urlencode(params)}",
|
|
84
|
+
)
|
|
85
|
+
return payload["versions"]
|
|
72
86
|
|
|
73
87
|
|
|
74
88
|
class MobileOpsResourceFilesClient:
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Tool permission policies. Approval decisions never modify configured rules.
|
|
2
|
+
Keep validators aligned with the shared conformance fixtures.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
TOOL_PERMISSIONS_CONFIG_KEY = "haiops_tool_permissions"
|
|
12
|
+
BUILTIN_TOOL_NAMES = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"ls",
|
|
15
|
+
"read_file",
|
|
16
|
+
"write_file",
|
|
17
|
+
"edit_file",
|
|
18
|
+
"delete",
|
|
19
|
+
"glob",
|
|
20
|
+
"grep",
|
|
21
|
+
"execute",
|
|
22
|
+
"eval",
|
|
23
|
+
"task",
|
|
24
|
+
"write_todos",
|
|
25
|
+
"request_user_input",
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _object(value: Any, keys: set[str]) -> dict:
|
|
31
|
+
if not isinstance(value, dict) or set(value) - keys:
|
|
32
|
+
raise ValueError("Invalid tool permissions object or unsupported field")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _rule(value: Any, named: bool = False) -> dict:
|
|
37
|
+
r = _object(
|
|
38
|
+
value, {"enabled", "permission_policy"} | ({"name"} if named else set())
|
|
39
|
+
)
|
|
40
|
+
if "enabled" in r and type(r["enabled"]) is not bool:
|
|
41
|
+
raise ValueError("enabled must be boolean")
|
|
42
|
+
if "permission_policy" in r:
|
|
43
|
+
p = _object(r["permission_policy"], {"type"})
|
|
44
|
+
if p.get("type") not in ("always_allow", "always_ask"):
|
|
45
|
+
raise ValueError("Unsupported permission policy")
|
|
46
|
+
return r
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_tool_permissions(value: Any, servers: list[str] | None = None) -> dict:
|
|
50
|
+
p = _object(value, {"version", "toolsets"})
|
|
51
|
+
if (
|
|
52
|
+
type(p.get("version")) is not int
|
|
53
|
+
or p["version"] != 1
|
|
54
|
+
or not isinstance(p.get("toolsets"), list)
|
|
55
|
+
or len(p["toolsets"]) > 128
|
|
56
|
+
):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"Tool permissions require version 1 and a toolsets array (max 128)"
|
|
59
|
+
)
|
|
60
|
+
seen = set()
|
|
61
|
+
for raw in p["toolsets"]:
|
|
62
|
+
s = _object(raw, {"type", "mcp_server_name", "default_config", "configs"})
|
|
63
|
+
if s.get("type") not in ("builtin", "mcp"):
|
|
64
|
+
raise ValueError("Unknown toolset type")
|
|
65
|
+
if s["type"] == "mcp":
|
|
66
|
+
server = s.get("mcp_server_name")
|
|
67
|
+
if (
|
|
68
|
+
not isinstance(server, str)
|
|
69
|
+
or not re.fullmatch(r"[A-Za-z0-9.-]+", server)
|
|
70
|
+
or (servers is not None and server not in servers)
|
|
71
|
+
):
|
|
72
|
+
raise ValueError("Unknown or invalid MCP server")
|
|
73
|
+
elif "mcp_server_name" in s:
|
|
74
|
+
raise ValueError("Only MCP toolsets accept mcp_server_name")
|
|
75
|
+
key = (s["type"], s.get("mcp_server_name"))
|
|
76
|
+
if key in seen:
|
|
77
|
+
raise ValueError("Duplicate toolset")
|
|
78
|
+
seen.add(key)
|
|
79
|
+
if "default_config" in s:
|
|
80
|
+
_rule(s["default_config"])
|
|
81
|
+
if "configs" in s:
|
|
82
|
+
if not isinstance(s["configs"], list) or len(s["configs"]) > 1024:
|
|
83
|
+
raise ValueError("configs must be an array (max 1024)")
|
|
84
|
+
names = set()
|
|
85
|
+
for raw_config in s["configs"]:
|
|
86
|
+
c = _rule(raw_config, True)
|
|
87
|
+
name = c.get("name")
|
|
88
|
+
if (
|
|
89
|
+
not isinstance(name, str)
|
|
90
|
+
or not re.fullmatch(r"[A-Za-z0-9_.-]{1,128}", name)
|
|
91
|
+
or name in names
|
|
92
|
+
):
|
|
93
|
+
raise ValueError("Invalid or duplicate tool name")
|
|
94
|
+
if s["type"] == "builtin" and name not in BUILTIN_TOOL_NAMES:
|
|
95
|
+
raise ValueError("Unknown built-in tool")
|
|
96
|
+
names.add(name)
|
|
97
|
+
return copy.deepcopy(p)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mobileops-managed-agent
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Server-side SDK for the MobileOPS Managed Agent API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/InnoMindX/deepagent/tree/main/managed-agent-sdk/python
|
|
6
6
|
Project-URL: Repository, https://github.com/InnoMindX/deepagent
|
|
@@ -2,6 +2,7 @@ MANIFEST.in
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/mobileops_managed_agent/__init__.py
|
|
5
|
+
src/mobileops_managed_agent/artifacts.py
|
|
5
6
|
src/mobileops_managed_agent/assistant_model.py
|
|
6
7
|
src/mobileops_managed_agent/assistants.py
|
|
7
8
|
src/mobileops_managed_agent/auth.py
|
|
@@ -9,10 +10,12 @@ src/mobileops_managed_agent/capabilities.py
|
|
|
9
10
|
src/mobileops_managed_agent/client.py
|
|
10
11
|
src/mobileops_managed_agent/credential_vault.py
|
|
11
12
|
src/mobileops_managed_agent/files.py
|
|
13
|
+
src/mobileops_managed_agent/model_retry.py
|
|
12
14
|
src/mobileops_managed_agent/resources.py
|
|
13
15
|
src/mobileops_managed_agent/response_schema.py
|
|
14
16
|
src/mobileops_managed_agent/sandbox_profile.py
|
|
15
17
|
src/mobileops_managed_agent/sandbox_profiles.py
|
|
18
|
+
src/mobileops_managed_agent/tool_permissions.py
|
|
16
19
|
src/mobileops_managed_agent/vaults.py
|
|
17
20
|
src/mobileops_managed_agent.egg-info/PKG-INFO
|
|
18
21
|
src/mobileops_managed_agent.egg-info/SOURCES.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{mobileops_managed_agent-0.3.0 → mobileops_managed_agent-0.3.2}/src/mobileops_managed_agent/files.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|