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.
Files changed (21) hide show
  1. {mobileops_managed_agent-0.2.0/src/mobileops_managed_agent.egg-info → mobileops_managed_agent-0.2.2}/PKG-INFO +1 -1
  2. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/pyproject.toml +1 -1
  3. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/__init__.py +20 -0
  4. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/assistant_model.py +48 -1
  5. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/assistants.py +21 -1
  6. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/capabilities.py +154 -2
  7. mobileops_managed_agent-0.2.2/src/mobileops_managed_agent/credential_vault.py +38 -0
  8. mobileops_managed_agent-0.2.2/src/mobileops_managed_agent/sandbox_profile.py +38 -0
  9. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2/src/mobileops_managed_agent.egg-info}/PKG-INFO +1 -1
  10. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent.egg-info/SOURCES.txt +2 -0
  11. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/MANIFEST.in +0 -0
  12. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/README.md +0 -0
  13. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/setup.cfg +0 -0
  14. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/auth.py +0 -0
  15. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/client.py +0 -0
  16. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/files.py +0 -0
  17. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/resources.py +0 -0
  18. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent/response_schema.py +0 -0
  19. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent.egg-info/dependency_links.txt +0 -0
  20. {mobileops_managed_agent-0.2.0 → mobileops_managed_agent-0.2.2}/src/mobileops_managed_agent.egg-info/requires.txt +0 -0
  21. {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.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mobileops-managed-agent"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Server-side SDK for the MobileOPS Managed Agent API."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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
- if bundle is None and not has_response_schema:
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 and haiops_response_schema"
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.0
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