mobileops-managed-agent 0.2.0__tar.gz → 0.2.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.2.0/src/mobileops_managed_agent.egg-info → mobileops_managed_agent-0.2.2}/PKG-INFO +1 -1
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/pyproject.toml +1 -1
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/__init__.py +20 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/assistant_model.py +48 -1
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/assistants.py +21 -1
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/capabilities.py +154 -2
- mobileops_managed_agent-0.2.2/src/mobileops_managed_agent/credential_vault.py +38 -0
- mobileops_managed_agent-0.2.2/src/mobileops_managed_agent/sandbox_profile.py +38 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2/src/mobileops_managed_agent.egg-info}/PKG-INFO +1 -1
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent.egg-info/SOURCES.txt +2 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/MANIFEST.in +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/README.md +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/setup.cfg +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/auth.py +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/client.py +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/files.py +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/resources.py +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/response_schema.py +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent.egg-info/dependency_links.txt +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent.egg-info/requires.txt +0 -0
- {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.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.2.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -14,6 +14,12 @@ from .capabilities import (
|
|
|
14
14
|
validate_capability_bundle,
|
|
15
15
|
)
|
|
16
16
|
from .client import MobileOpsAgentClient, MobileOpsManagedAgentClient
|
|
17
|
+
from .credential_vault import (
|
|
18
|
+
CREDENTIAL_VAULT_CONFIG_KEY,
|
|
19
|
+
CREDENTIAL_VAULT_ID_PREFIX,
|
|
20
|
+
CredentialVaultReference,
|
|
21
|
+
validate_credential_vault,
|
|
22
|
+
)
|
|
17
23
|
from .files import (
|
|
18
24
|
MobileOpsFilesClient,
|
|
19
25
|
MobileOpsFilesError,
|
|
@@ -30,6 +36,12 @@ from .response_schema import (
|
|
|
30
36
|
JsonSchema,
|
|
31
37
|
validate_response_schema,
|
|
32
38
|
)
|
|
39
|
+
from .sandbox_profile import (
|
|
40
|
+
SANDBOX_PROFILE_CONFIG_KEY,
|
|
41
|
+
SANDBOX_PROFILE_ID_PREFIX,
|
|
42
|
+
SandboxProfileReference,
|
|
43
|
+
validate_sandbox_profile,
|
|
44
|
+
)
|
|
33
45
|
|
|
34
46
|
__all__ = [
|
|
35
47
|
"MobileOpsManagedAgentClient",
|
|
@@ -55,4 +67,12 @@ __all__ = [
|
|
|
55
67
|
"JsonSchema",
|
|
56
68
|
"RESPONSE_SCHEMA_CONFIG_KEY",
|
|
57
69
|
"validate_response_schema",
|
|
70
|
+
"SANDBOX_PROFILE_CONFIG_KEY",
|
|
71
|
+
"SANDBOX_PROFILE_ID_PREFIX",
|
|
72
|
+
"SandboxProfileReference",
|
|
73
|
+
"validate_sandbox_profile",
|
|
74
|
+
"CREDENTIAL_VAULT_CONFIG_KEY",
|
|
75
|
+
"CREDENTIAL_VAULT_ID_PREFIX",
|
|
76
|
+
"CredentialVaultReference",
|
|
77
|
+
"validate_credential_vault",
|
|
58
78
|
]
|
|
@@ -9,11 +9,19 @@ from .capabilities import (
|
|
|
9
9
|
MobileOpsAssistantConfig,
|
|
10
10
|
validate_capability_bundle,
|
|
11
11
|
)
|
|
12
|
+
from .credential_vault import (
|
|
13
|
+
CREDENTIAL_VAULT_CONFIG_KEY,
|
|
14
|
+
validate_credential_vault,
|
|
15
|
+
)
|
|
12
16
|
from .response_schema import (
|
|
13
17
|
RESPONSE_SCHEMA_CONFIG_KEY,
|
|
14
18
|
JsonSchema,
|
|
15
19
|
validate_response_schema,
|
|
16
20
|
)
|
|
21
|
+
from .sandbox_profile import (
|
|
22
|
+
SANDBOX_PROFILE_CONFIG_KEY,
|
|
23
|
+
validate_sandbox_profile,
|
|
24
|
+
)
|
|
17
25
|
|
|
18
26
|
|
|
19
27
|
class ContextHubAttachment(TypedDict):
|
|
@@ -43,6 +51,8 @@ class MobileOpsAssistant(TypedDict, total=False):
|
|
|
43
51
|
file_resources: list[FileResourceAttachment]
|
|
44
52
|
mcp_servers: dict[str, CapabilityMcpServer]
|
|
45
53
|
response_schema: JsonSchema
|
|
54
|
+
sandbox_profile: str
|
|
55
|
+
credential_vault: str
|
|
46
56
|
raw: Any
|
|
47
57
|
created_at: str
|
|
48
58
|
updated_at: str
|
|
@@ -116,7 +126,14 @@ def to_capability_bundle(**kwargs: Any) -> CapabilityBundle | None:
|
|
|
116
126
|
def to_wire_config(**kwargs: Any) -> MobileOpsAssistantConfig | None:
|
|
117
127
|
bundle = to_capability_bundle(**kwargs)
|
|
118
128
|
has_response_schema = "response_schema" in kwargs
|
|
119
|
-
|
|
129
|
+
has_sandbox_profile = "sandbox_profile" in kwargs
|
|
130
|
+
has_credential_vault = "credential_vault" in kwargs
|
|
131
|
+
if (
|
|
132
|
+
bundle is None
|
|
133
|
+
and not has_response_schema
|
|
134
|
+
and not has_sandbox_profile
|
|
135
|
+
and not has_credential_vault
|
|
136
|
+
):
|
|
120
137
|
return None
|
|
121
138
|
configurable: dict[str, Any] = {}
|
|
122
139
|
if bundle is not None:
|
|
@@ -126,6 +143,16 @@ def to_wire_config(**kwargs: Any) -> MobileOpsAssistantConfig | None:
|
|
|
126
143
|
configurable[RESPONSE_SCHEMA_CONFIG_KEY] = validate_response_schema(
|
|
127
144
|
response_schema
|
|
128
145
|
)
|
|
146
|
+
sandbox_profile = kwargs.get("sandbox_profile")
|
|
147
|
+
if has_sandbox_profile and sandbox_profile is not None:
|
|
148
|
+
configurable[SANDBOX_PROFILE_CONFIG_KEY] = validate_sandbox_profile(
|
|
149
|
+
{"ref": sandbox_profile}
|
|
150
|
+
)
|
|
151
|
+
credential_vault = kwargs.get("credential_vault")
|
|
152
|
+
if has_credential_vault and credential_vault is not None:
|
|
153
|
+
configurable[CREDENTIAL_VAULT_CONFIG_KEY] = validate_credential_vault(
|
|
154
|
+
{"ref": credential_vault}
|
|
155
|
+
)
|
|
129
156
|
return {"configurable": configurable} # type: ignore[return-value]
|
|
130
157
|
|
|
131
158
|
|
|
@@ -241,6 +268,22 @@ def from_wire_assistant(raw: object) -> MobileOpsAssistant:
|
|
|
241
268
|
if response_schema_raw is not None
|
|
242
269
|
else None
|
|
243
270
|
)
|
|
271
|
+
sandbox_profile_raw = (
|
|
272
|
+
configurable.get(SANDBOX_PROFILE_CONFIG_KEY) if configurable else None
|
|
273
|
+
)
|
|
274
|
+
sandbox_profile = (
|
|
275
|
+
validate_sandbox_profile(sandbox_profile_raw)["ref"]
|
|
276
|
+
if sandbox_profile_raw is not None
|
|
277
|
+
else None
|
|
278
|
+
)
|
|
279
|
+
credential_vault_raw = (
|
|
280
|
+
configurable.get(CREDENTIAL_VAULT_CONFIG_KEY) if configurable else None
|
|
281
|
+
)
|
|
282
|
+
credential_vault = (
|
|
283
|
+
validate_credential_vault(credential_vault_raw)["ref"]
|
|
284
|
+
if credential_vault_raw is not None
|
|
285
|
+
else None
|
|
286
|
+
)
|
|
244
287
|
|
|
245
288
|
if bundle:
|
|
246
289
|
hub = bundle.get("context_hub_resources") or []
|
|
@@ -282,6 +325,10 @@ def from_wire_assistant(raw: object) -> MobileOpsAssistant:
|
|
|
282
325
|
}
|
|
283
326
|
if response_schema is not None:
|
|
284
327
|
product["response_schema"] = response_schema
|
|
328
|
+
if sandbox_profile is not None:
|
|
329
|
+
product["sandbox_profile"] = sandbox_profile
|
|
330
|
+
if credential_vault is not None:
|
|
331
|
+
product["credential_vault"] = credential_vault
|
|
285
332
|
if isinstance(doc.get("name"), str):
|
|
286
333
|
product["name"] = doc["name"]
|
|
287
334
|
if isinstance(doc.get("description"), str):
|
|
@@ -50,6 +50,8 @@ class MobileOpsAssistantsClient:
|
|
|
50
50
|
file_resources: list[FileResourceAttachment] | None = None,
|
|
51
51
|
mcp_servers: dict[str, CapabilityMcpServer] | None = None,
|
|
52
52
|
response_schema: JsonSchema | None = None,
|
|
53
|
+
sandbox_profile: str | None = None,
|
|
54
|
+
credential_vault: str | None = None,
|
|
53
55
|
assistant_id: str | None = None,
|
|
54
56
|
if_exists: OnConflictBehavior | None = None,
|
|
55
57
|
context: Context | None = None,
|
|
@@ -87,6 +89,10 @@ class MobileOpsAssistantsClient:
|
|
|
87
89
|
product_kwargs["mcp_servers"] = mcp_servers
|
|
88
90
|
if response_schema is not None:
|
|
89
91
|
product_kwargs["response_schema"] = response_schema
|
|
92
|
+
if sandbox_profile is not None:
|
|
93
|
+
product_kwargs["sandbox_profile"] = sandbox_profile
|
|
94
|
+
if credential_vault is not None:
|
|
95
|
+
product_kwargs["credential_vault"] = credential_vault
|
|
90
96
|
|
|
91
97
|
wire = to_wire_create_kwargs(**product_kwargs)
|
|
92
98
|
native_kwargs = {
|
|
@@ -164,6 +170,8 @@ class MobileOpsAssistantsClient:
|
|
|
164
170
|
file_resources: list[FileResourceAttachment] | None = None,
|
|
165
171
|
mcp_servers: dict[str, CapabilityMcpServer] | None = None,
|
|
166
172
|
response_schema: JsonSchema | None | _NotGiven = _NOT_GIVEN,
|
|
173
|
+
sandbox_profile: str | None | _NotGiven = _NOT_GIVEN,
|
|
174
|
+
credential_vault: str | None | _NotGiven = _NOT_GIVEN,
|
|
167
175
|
context: Context | None = None,
|
|
168
176
|
headers: Mapping[str, str] | None = None,
|
|
169
177
|
params: QueryParamTypes | None = None,
|
|
@@ -202,7 +210,9 @@ class MobileOpsAssistantsClient:
|
|
|
202
210
|
file_resources,
|
|
203
211
|
mcp_servers,
|
|
204
212
|
)
|
|
205
|
-
) or not isinstance(response_schema, _NotGiven)
|
|
213
|
+
) or not isinstance(response_schema, _NotGiven) or not isinstance(
|
|
214
|
+
sandbox_profile, _NotGiven
|
|
215
|
+
) or not isinstance(credential_vault, _NotGiven)
|
|
206
216
|
if changes_config:
|
|
207
217
|
current = await self.get(
|
|
208
218
|
assistant_id,
|
|
@@ -233,6 +243,16 @@ class MobileOpsAssistantsClient:
|
|
|
233
243
|
if not isinstance(response_schema, _NotGiven)
|
|
234
244
|
else current.get("response_schema")
|
|
235
245
|
)
|
|
246
|
+
product_kwargs["sandbox_profile"] = (
|
|
247
|
+
sandbox_profile
|
|
248
|
+
if not isinstance(sandbox_profile, _NotGiven)
|
|
249
|
+
else current.get("sandbox_profile")
|
|
250
|
+
)
|
|
251
|
+
product_kwargs["credential_vault"] = (
|
|
252
|
+
credential_vault
|
|
253
|
+
if not isinstance(credential_vault, _NotGiven)
|
|
254
|
+
else current.get("credential_vault")
|
|
255
|
+
)
|
|
236
256
|
|
|
237
257
|
wire = to_wire_update_kwargs(**product_kwargs)
|
|
238
258
|
updated = await self._native.update(assistant_id, **wire)
|
|
@@ -4,7 +4,17 @@ import re
|
|
|
4
4
|
from typing import Literal, NotRequired, Required, TypedDict
|
|
5
5
|
from urllib.parse import urlparse
|
|
6
6
|
|
|
7
|
+
from .credential_vault import (
|
|
8
|
+
CREDENTIAL_VAULT_CONFIG_KEY,
|
|
9
|
+
CredentialVaultReference,
|
|
10
|
+
validate_credential_vault,
|
|
11
|
+
)
|
|
7
12
|
from .response_schema import JsonSchema, validate_response_schema
|
|
13
|
+
from .sandbox_profile import (
|
|
14
|
+
SANDBOX_PROFILE_CONFIG_KEY,
|
|
15
|
+
SandboxProfileReference,
|
|
16
|
+
validate_sandbox_profile,
|
|
17
|
+
)
|
|
8
18
|
|
|
9
19
|
|
|
10
20
|
# Capability Bundle validation is a cross-project protocol contract. Any rule
|
|
@@ -47,6 +57,9 @@ class StdioMcpServer(TypedDict, total=False):
|
|
|
47
57
|
class RemoteMcpServer(TypedDict, total=False):
|
|
48
58
|
type: Literal["http", "sse"]
|
|
49
59
|
url: Required[str]
|
|
60
|
+
# Headers sent to the remote MCP server. Values may reference jail env vars
|
|
61
|
+
# (e.g. "$GRAFANA_TOKEN") so credential-vault placeholders can be substituted.
|
|
62
|
+
headers: dict[str, str]
|
|
50
63
|
|
|
51
64
|
|
|
52
65
|
CapabilityMcpServer = StdioMcpServer | RemoteMcpServer
|
|
@@ -65,6 +78,8 @@ class CapabilityBundle(TypedDict, total=False):
|
|
|
65
78
|
class MobileOpsAssistantConfigurable(TypedDict, total=False):
|
|
66
79
|
haiops_capability_bundle: CapabilityBundle
|
|
67
80
|
haiops_response_schema: JsonSchema | None
|
|
81
|
+
haiops_sandbox_profile: SandboxProfileReference
|
|
82
|
+
haiops_credential_vault: CredentialVaultReference
|
|
68
83
|
|
|
69
84
|
|
|
70
85
|
class MobileOpsAssistantConfig(TypedDict, total=False):
|
|
@@ -231,12 +246,142 @@ def validate_capability_bundle(value: object) -> None:
|
|
|
231
246
|
if "clear_env" in server and not isinstance(server["clear_env"], bool):
|
|
232
247
|
raise TypeError(f"mcp.mcpServers.{name}.clear_env must be boolean")
|
|
233
248
|
elif server_type in {"http", "sse"}:
|
|
234
|
-
_allowed_keys(server, {"type", "url"}, f"mcp.mcpServers.{name}")
|
|
249
|
+
_allowed_keys(server, {"type", "url", "headers"}, f"mcp.mcpServers.{name}")
|
|
235
250
|
_http_url(server.get("url"), f"mcp.mcpServers.{name}.url")
|
|
236
251
|
if "command" in server:
|
|
237
252
|
raise TypeError(f"mcp.mcpServers.{name} must not define command")
|
|
253
|
+
if "headers" in server:
|
|
254
|
+
headers = _object(server["headers"], f"mcp.mcpServers.{name}.headers")
|
|
255
|
+
if len(headers) > 32:
|
|
256
|
+
raise TypeError(
|
|
257
|
+
f"mcp.mcpServers.{name}.headers must have at most 32 entries"
|
|
258
|
+
)
|
|
259
|
+
total_value_bytes = 0
|
|
260
|
+
for key, value in headers.items():
|
|
261
|
+
if not isinstance(key, str) or not re.fullmatch(
|
|
262
|
+
r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+", key
|
|
263
|
+
):
|
|
264
|
+
raise TypeError(
|
|
265
|
+
f"mcp.mcpServers.{name}.headers contains invalid "
|
|
266
|
+
f"header name: {key!r}"
|
|
267
|
+
)
|
|
268
|
+
if not isinstance(value, str):
|
|
269
|
+
raise TypeError(
|
|
270
|
+
f"mcp.mcpServers.{name}.headers.{key} must be a string"
|
|
271
|
+
)
|
|
272
|
+
if any(ord(ch) < 0x20 or ord(ch) == 0x7F for ch in value):
|
|
273
|
+
raise TypeError(
|
|
274
|
+
f"mcp.mcpServers.{name}.headers.{key} must not contain "
|
|
275
|
+
"control characters"
|
|
276
|
+
)
|
|
277
|
+
value_bytes = len(value.encode("utf-8"))
|
|
278
|
+
if value_bytes > 8 * 1024:
|
|
279
|
+
raise TypeError(
|
|
280
|
+
f"mcp.mcpServers.{name}.headers.{key} must be at most "
|
|
281
|
+
"8192 bytes"
|
|
282
|
+
)
|
|
283
|
+
total_value_bytes += value_bytes
|
|
284
|
+
if total_value_bytes > 48 * 1024:
|
|
285
|
+
raise TypeError(
|
|
286
|
+
f"mcp.mcpServers.{name}.headers total value size must "
|
|
287
|
+
"be at most 49152 bytes"
|
|
288
|
+
)
|
|
238
289
|
else:
|
|
239
290
|
raise TypeError(f"mcp.mcpServers.{name}.type must be stdio, http, or sse")
|
|
291
|
+
_assert_mcp_gateway_config_launchable(servers)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
_MAX_AGENTGATEWAY_CONFIG_BYTES = 100_000
|
|
295
|
+
# Matches backend size budget for processd backendTunnel injection
|
|
296
|
+
# (DNS-max hostname + port; not just 127.0.0.1).
|
|
297
|
+
_SIZE_BUDGET_PROXY_HOST = f"{'x' * 253}:65535"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _size_budget_upstream_host(url: object) -> str | None:
|
|
301
|
+
"""Mirror backend auto-Host injection for tunnel absolute-form authority."""
|
|
302
|
+
if not isinstance(url, str) or not url.strip():
|
|
303
|
+
return None
|
|
304
|
+
from urllib.parse import urlparse
|
|
305
|
+
|
|
306
|
+
text = url.strip()
|
|
307
|
+
parsed = urlparse(text if "://" in text else f"http://{text}")
|
|
308
|
+
host = parsed.hostname
|
|
309
|
+
if not host:
|
|
310
|
+
return None
|
|
311
|
+
port = parsed.port
|
|
312
|
+
if port is None:
|
|
313
|
+
port = 443 if (parsed.scheme or "http").lower() == "https" else 80
|
|
314
|
+
if not (1 <= port <= 65535):
|
|
315
|
+
return None
|
|
316
|
+
if ":" in host:
|
|
317
|
+
return f"[{host}]:{port}"
|
|
318
|
+
return f"{host}:{port}"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _assert_mcp_gateway_config_launchable(servers: dict) -> None:
|
|
322
|
+
"""Mirror backend agentgateway config size guard (argv limit for `-c`)."""
|
|
323
|
+
targets: list[dict] = []
|
|
324
|
+
for name, item in sorted(servers.items()):
|
|
325
|
+
server = item if isinstance(item, dict) else {}
|
|
326
|
+
inferred = (
|
|
327
|
+
"stdio" if "command" in server else "http" if "url" in server else None
|
|
328
|
+
)
|
|
329
|
+
server_type = server.get("type", inferred)
|
|
330
|
+
if server_type == "stdio":
|
|
331
|
+
stdio: dict = {"cmd": server.get("command"), "clear_env": server.get("clear_env", False)}
|
|
332
|
+
if "args" in server:
|
|
333
|
+
stdio["args"] = server["args"]
|
|
334
|
+
if "env" in server:
|
|
335
|
+
stdio["env"] = server["env"]
|
|
336
|
+
targets.append({"name": name, "stdio": stdio})
|
|
337
|
+
continue
|
|
338
|
+
# Budget processd backendTunnel + auto Host injected at sandbox setup.
|
|
339
|
+
policies: dict = {
|
|
340
|
+
"backendTunnel": {"proxy": {"host": _SIZE_BUDGET_PROXY_HOST}},
|
|
341
|
+
}
|
|
342
|
+
headers = server.get("headers")
|
|
343
|
+
header_set: dict = dict(headers) if isinstance(headers, dict) else {}
|
|
344
|
+
if not any(str(k).lower() == "host" for k in header_set):
|
|
345
|
+
upstream_host = _size_budget_upstream_host(server.get("url"))
|
|
346
|
+
if upstream_host:
|
|
347
|
+
header_set["Host"] = upstream_host
|
|
348
|
+
if header_set:
|
|
349
|
+
policies["requestHeaderModifier"] = {"set": header_set}
|
|
350
|
+
targets.append(
|
|
351
|
+
{
|
|
352
|
+
"name": name,
|
|
353
|
+
("sse" if server_type == "sse" else "mcp"): {
|
|
354
|
+
"host": server.get("url")
|
|
355
|
+
},
|
|
356
|
+
"policies": policies,
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
config = {
|
|
360
|
+
"mcp": {
|
|
361
|
+
"port": 3009,
|
|
362
|
+
"statefulMode": "stateful",
|
|
363
|
+
"prefixMode": "always",
|
|
364
|
+
"failureMode": "failOpen",
|
|
365
|
+
"policies": {
|
|
366
|
+
"cors": {
|
|
367
|
+
"allowOrigins": ["*"],
|
|
368
|
+
"allowHeaders": ["*"],
|
|
369
|
+
"exposeHeaders": ["Mcp-Session-Id"],
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
"targets": targets,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
import json
|
|
376
|
+
|
|
377
|
+
size = len(
|
|
378
|
+
json.dumps(config, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
379
|
+
)
|
|
380
|
+
if size > _MAX_AGENTGATEWAY_CONFIG_BYTES:
|
|
381
|
+
raise TypeError(
|
|
382
|
+
f"MCP gateway config is too large to start ({size} bytes; "
|
|
383
|
+
f"max {_MAX_AGENTGATEWAY_CONFIG_BYTES}). Reduce remote MCP headers."
|
|
384
|
+
)
|
|
240
385
|
|
|
241
386
|
|
|
242
387
|
def validate_assistant_config(config: MobileOpsAssistantConfig | None) -> None:
|
|
@@ -253,12 +398,19 @@ def validate_assistant_config(config: MobileOpsAssistantConfig | None) -> None:
|
|
|
253
398
|
if set(configurable) - {
|
|
254
399
|
"haiops_capability_bundle",
|
|
255
400
|
"haiops_response_schema",
|
|
401
|
+
SANDBOX_PROFILE_CONFIG_KEY,
|
|
402
|
+
CREDENTIAL_VAULT_CONFIG_KEY,
|
|
256
403
|
}:
|
|
257
404
|
raise TypeError(
|
|
258
405
|
"Assistant config.configurable only supports "
|
|
259
|
-
"haiops_capability_bundle
|
|
406
|
+
"haiops_capability_bundle, haiops_response_schema, "
|
|
407
|
+
f"{SANDBOX_PROFILE_CONFIG_KEY} and {CREDENTIAL_VAULT_CONFIG_KEY}"
|
|
260
408
|
)
|
|
261
409
|
if "haiops_capability_bundle" in configurable:
|
|
262
410
|
validate_capability_bundle(configurable["haiops_capability_bundle"])
|
|
263
411
|
if configurable.get("haiops_response_schema") is not None:
|
|
264
412
|
validate_response_schema(configurable["haiops_response_schema"])
|
|
413
|
+
if SANDBOX_PROFILE_CONFIG_KEY in configurable:
|
|
414
|
+
validate_sandbox_profile(configurable[SANDBOX_PROFILE_CONFIG_KEY])
|
|
415
|
+
if CREDENTIAL_VAULT_CONFIG_KEY in configurable:
|
|
416
|
+
validate_credential_vault(configurable[CREDENTIAL_VAULT_CONFIG_KEY])
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Credential Vault reference validation (MobileOPS assistant config contract).
|
|
2
|
+
|
|
3
|
+
``haiops_credential_vault`` is an independent configurable key holding a
|
|
4
|
+
reference to a Context Hub credential vault. It is NOT a capability bundle
|
|
5
|
+
field, so the bundle validators and test-fixtures/capability-bundle are
|
|
6
|
+
unaffected.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, TypeAlias
|
|
12
|
+
|
|
13
|
+
CREDENTIAL_VAULT_CONFIG_KEY = "haiops_credential_vault"
|
|
14
|
+
CREDENTIAL_VAULT_ID_PREFIX = "vlt_"
|
|
15
|
+
|
|
16
|
+
CredentialVaultReference: TypeAlias = dict[str, str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_credential_vault(raw: Any) -> CredentialVaultReference:
|
|
20
|
+
"""Validate the ``haiops_credential_vault`` assistant-config value.
|
|
21
|
+
|
|
22
|
+
Shape: ``{"ref": "vlt_..."}``. Only the reference is persisted on the
|
|
23
|
+
assistant; the credentials live in Context Hub and are resolved
|
|
24
|
+
server-side at run time. Inline credential fields are rejected.
|
|
25
|
+
"""
|
|
26
|
+
if not isinstance(raw, dict):
|
|
27
|
+
raise TypeError("Credential vault must be a JSON object")
|
|
28
|
+
unknown = sorted(set(raw) - {"ref"})
|
|
29
|
+
if unknown:
|
|
30
|
+
raise TypeError(f"Credential vault contains unsupported field: {unknown[0]}")
|
|
31
|
+
ref = raw.get("ref")
|
|
32
|
+
if not isinstance(ref, str) or not ref.strip():
|
|
33
|
+
raise TypeError("Credential vault ref must be a non-empty string")
|
|
34
|
+
if not ref.startswith(CREDENTIAL_VAULT_ID_PREFIX):
|
|
35
|
+
raise TypeError(
|
|
36
|
+
f"Credential vault ref must start with {CREDENTIAL_VAULT_ID_PREFIX!r}"
|
|
37
|
+
)
|
|
38
|
+
return {"ref": ref}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Sandbox Profile reference validation (MobileOPS assistant config contract).
|
|
2
|
+
|
|
3
|
+
``haiops_sandbox_profile`` is an independent configurable key holding a
|
|
4
|
+
reference to a Context Hub sandbox profile. It is NOT a capability bundle
|
|
5
|
+
field, so the bundle validators and test-fixtures/capability-bundle are
|
|
6
|
+
unaffected.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, TypeAlias
|
|
12
|
+
|
|
13
|
+
SANDBOX_PROFILE_CONFIG_KEY = "haiops_sandbox_profile"
|
|
14
|
+
SANDBOX_PROFILE_ID_PREFIX = "sbp_"
|
|
15
|
+
|
|
16
|
+
SandboxProfileReference: TypeAlias = dict[str, str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_sandbox_profile(raw: Any) -> SandboxProfileReference:
|
|
20
|
+
"""Validate the ``haiops_sandbox_profile`` assistant-config value.
|
|
21
|
+
|
|
22
|
+
Shape: ``{"ref": "sbp_..."}``. Only the reference is persisted on the
|
|
23
|
+
assistant; the network policy body lives in Context Hub and is resolved
|
|
24
|
+
server-side at run time. Inline policy fields are rejected.
|
|
25
|
+
"""
|
|
26
|
+
if not isinstance(raw, dict):
|
|
27
|
+
raise TypeError("Sandbox profile must be a JSON object")
|
|
28
|
+
unknown = sorted(set(raw) - {"ref"})
|
|
29
|
+
if unknown:
|
|
30
|
+
raise TypeError(f"Sandbox profile contains unsupported field: {unknown[0]}")
|
|
31
|
+
ref = raw.get("ref")
|
|
32
|
+
if not isinstance(ref, str) or not ref.strip():
|
|
33
|
+
raise TypeError("Sandbox profile ref must be a non-empty string")
|
|
34
|
+
if not ref.startswith(SANDBOX_PROFILE_ID_PREFIX):
|
|
35
|
+
raise TypeError(
|
|
36
|
+
f"Sandbox profile ref must start with {SANDBOX_PROFILE_ID_PREFIX!r}"
|
|
37
|
+
)
|
|
38
|
+
return {"ref": ref}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mobileops-managed-agent
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -7,9 +7,11 @@ src/mobileops_managed_agent/assistants.py
|
|
|
7
7
|
src/mobileops_managed_agent/auth.py
|
|
8
8
|
src/mobileops_managed_agent/capabilities.py
|
|
9
9
|
src/mobileops_managed_agent/client.py
|
|
10
|
+
src/mobileops_managed_agent/credential_vault.py
|
|
10
11
|
src/mobileops_managed_agent/files.py
|
|
11
12
|
src/mobileops_managed_agent/resources.py
|
|
12
13
|
src/mobileops_managed_agent/response_schema.py
|
|
14
|
+
src/mobileops_managed_agent/sandbox_profile.py
|
|
13
15
|
src/mobileops_managed_agent.egg-info/PKG-INFO
|
|
14
16
|
src/mobileops_managed_agent.egg-info/SOURCES.txt
|
|
15
17
|
src/mobileops_managed_agent.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.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
|