mobileops-managed-agent 0.2.2__tar.gz → 0.3.0__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.2/src/mobileops_managed_agent.egg-info → mobileops_managed_agent-0.3.0}/PKG-INFO +19 -2
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/README.md +18 -1
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/pyproject.toml +1 -1
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/__init__.py +38 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/client.py +21 -1
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/credential_vault.py +1 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/resources.py +0 -1
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/sandbox_profile.py +1 -0
- mobileops_managed_agent-0.3.0/src/mobileops_managed_agent/sandbox_profiles.py +148 -0
- mobileops_managed_agent-0.3.0/src/mobileops_managed_agent/vaults.py +231 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0/src/mobileops_managed_agent.egg-info}/PKG-INFO +19 -2
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent.egg-info/SOURCES.txt +2 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/MANIFEST.in +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/setup.cfg +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/assistant_model.py +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/assistants.py +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/auth.py +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/capabilities.py +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/files.py +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/response_schema.py +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent.egg-info/dependency_links.txt +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent.egg-info/requires.txt +0 -0
- {mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/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
|
+
Version: 0.3.0
|
|
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
|
|
@@ -18,11 +18,13 @@ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
|
18
18
|
|
|
19
19
|
Server-side Python SDK for the MobileOPS Managed Agent API.
|
|
20
20
|
|
|
21
|
-
The SDK exposes
|
|
21
|
+
The SDK exposes five surfaces:
|
|
22
22
|
|
|
23
23
|
- `client.agent` wraps the LangGraph runtime client and exposes the supported Assistant CRUD methods at `client.agent.assistants`.
|
|
24
24
|
- `client.files` exposes MobileOPS Sandbox Workspace file routes.
|
|
25
25
|
- `client.resources` manages Context Hub resources, versions, and latest-version files.
|
|
26
|
+
- `client.vaults` manages credential vaults and their write-only credentials.
|
|
27
|
+
- `client.sandbox_profiles` manages sandbox profiles (运行环境).
|
|
26
28
|
|
|
27
29
|
Assistant create and update retain LangGraph's ordinary arguments, while
|
|
28
30
|
persisted configuration is exposed through product fields. Pass
|
|
@@ -37,6 +39,8 @@ therefore only affects Runs started on **new** Threads; existing Threads keep
|
|
|
37
39
|
the schema captured at their creation.
|
|
38
40
|
|
|
39
41
|
```py
|
|
42
|
+
import os
|
|
43
|
+
|
|
40
44
|
from mobileops_managed_agent import MobileOpsManagedAgentClient
|
|
41
45
|
|
|
42
46
|
client = MobileOpsManagedAgentClient(api_url=api_url, api_key=api_key)
|
|
@@ -45,12 +49,25 @@ thread = await client.agent.threads.create()
|
|
|
45
49
|
await client.agent.runs.create(thread["thread_id"], assistant_id, input=input)
|
|
46
50
|
|
|
47
51
|
skill = await client.resources.create("skills", zip_bytes) # slug inferred
|
|
52
|
+
profile = await client.sandbox_profiles.create(
|
|
53
|
+
name="prod",
|
|
54
|
+
spec={"network": {"allow": ["*.example.com"]}},
|
|
55
|
+
)
|
|
56
|
+
vault = await client.vaults.create(name="prod")
|
|
57
|
+
await client.vaults.credentials.create(
|
|
58
|
+
vault["id"],
|
|
59
|
+
name="GRAFANA_TOKEN",
|
|
60
|
+
value=os.environ["GRAFANA_TOKEN"],
|
|
61
|
+
allowed_hosts=["grafana.example.com"],
|
|
62
|
+
)
|
|
48
63
|
assistant = await client.agent.assistants.create(
|
|
49
64
|
name="Operations agent",
|
|
50
65
|
skills=[{
|
|
51
66
|
"ref": skill["ref"], # e.g. incident-response@sha256-…
|
|
52
67
|
"target_path": "/workspace/skills", # zip already contains {slug}/
|
|
53
68
|
}],
|
|
69
|
+
sandbox_profile=profile["id"],
|
|
70
|
+
credential_vault=vault["id"],
|
|
54
71
|
)
|
|
55
72
|
|
|
56
73
|
await client.resources.versions.create("skills", skill["slug"], next_zip_bytes)
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Server-side Python SDK for the MobileOPS Managed Agent API.
|
|
4
4
|
|
|
5
|
-
The SDK exposes
|
|
5
|
+
The SDK exposes five surfaces:
|
|
6
6
|
|
|
7
7
|
- `client.agent` wraps the LangGraph runtime client and exposes the supported Assistant CRUD methods at `client.agent.assistants`.
|
|
8
8
|
- `client.files` exposes MobileOPS Sandbox Workspace file routes.
|
|
9
9
|
- `client.resources` manages Context Hub resources, versions, and latest-version files.
|
|
10
|
+
- `client.vaults` manages credential vaults and their write-only credentials.
|
|
11
|
+
- `client.sandbox_profiles` manages sandbox profiles (运行环境).
|
|
10
12
|
|
|
11
13
|
Assistant create and update retain LangGraph's ordinary arguments, while
|
|
12
14
|
persisted configuration is exposed through product fields. Pass
|
|
@@ -21,6 +23,8 @@ therefore only affects Runs started on **new** Threads; existing Threads keep
|
|
|
21
23
|
the schema captured at their creation.
|
|
22
24
|
|
|
23
25
|
```py
|
|
26
|
+
import os
|
|
27
|
+
|
|
24
28
|
from mobileops_managed_agent import MobileOpsManagedAgentClient
|
|
25
29
|
|
|
26
30
|
client = MobileOpsManagedAgentClient(api_url=api_url, api_key=api_key)
|
|
@@ -29,12 +33,25 @@ thread = await client.agent.threads.create()
|
|
|
29
33
|
await client.agent.runs.create(thread["thread_id"], assistant_id, input=input)
|
|
30
34
|
|
|
31
35
|
skill = await client.resources.create("skills", zip_bytes) # slug inferred
|
|
36
|
+
profile = await client.sandbox_profiles.create(
|
|
37
|
+
name="prod",
|
|
38
|
+
spec={"network": {"allow": ["*.example.com"]}},
|
|
39
|
+
)
|
|
40
|
+
vault = await client.vaults.create(name="prod")
|
|
41
|
+
await client.vaults.credentials.create(
|
|
42
|
+
vault["id"],
|
|
43
|
+
name="GRAFANA_TOKEN",
|
|
44
|
+
value=os.environ["GRAFANA_TOKEN"],
|
|
45
|
+
allowed_hosts=["grafana.example.com"],
|
|
46
|
+
)
|
|
32
47
|
assistant = await client.agent.assistants.create(
|
|
33
48
|
name="Operations agent",
|
|
34
49
|
skills=[{
|
|
35
50
|
"ref": skill["ref"], # e.g. incident-response@sha256-…
|
|
36
51
|
"target_path": "/workspace/skills", # zip already contains {slug}/
|
|
37
52
|
}],
|
|
53
|
+
sandbox_profile=profile["id"],
|
|
54
|
+
credential_vault=vault["id"],
|
|
38
55
|
)
|
|
39
56
|
|
|
40
57
|
await client.resources.versions.create("skills", skill["slug"], next_zip_bytes)
|
|
@@ -17,6 +17,7 @@ from .client import MobileOpsAgentClient, MobileOpsManagedAgentClient
|
|
|
17
17
|
from .credential_vault import (
|
|
18
18
|
CREDENTIAL_VAULT_CONFIG_KEY,
|
|
19
19
|
CREDENTIAL_VAULT_ID_PREFIX,
|
|
20
|
+
SECRET_REDACTED_VALUE,
|
|
20
21
|
CredentialVaultReference,
|
|
21
22
|
validate_credential_vault,
|
|
22
23
|
)
|
|
@@ -39,9 +40,29 @@ from .response_schema import (
|
|
|
39
40
|
from .sandbox_profile import (
|
|
40
41
|
SANDBOX_PROFILE_CONFIG_KEY,
|
|
41
42
|
SANDBOX_PROFILE_ID_PREFIX,
|
|
43
|
+
SYSTEM_DEFAULT_PROFILE_ID,
|
|
42
44
|
SandboxProfileReference,
|
|
43
45
|
validate_sandbox_profile,
|
|
44
46
|
)
|
|
47
|
+
from .sandbox_profiles import (
|
|
48
|
+
MobileOpsSandboxProfilesClient,
|
|
49
|
+
MobileOpsSandboxProfilesError,
|
|
50
|
+
SandboxProfile,
|
|
51
|
+
SandboxProfileCreateInput,
|
|
52
|
+
SandboxProfileNetwork,
|
|
53
|
+
SandboxProfileSpec,
|
|
54
|
+
SandboxProfileUpdateInput,
|
|
55
|
+
)
|
|
56
|
+
from .vaults import (
|
|
57
|
+
MobileOpsVaultsClient,
|
|
58
|
+
MobileOpsVaultsError,
|
|
59
|
+
Vault,
|
|
60
|
+
VaultCreateInput,
|
|
61
|
+
VaultCredential,
|
|
62
|
+
VaultCredentialInput,
|
|
63
|
+
VaultCredentialUpdateInput,
|
|
64
|
+
VaultUpdateInput,
|
|
65
|
+
)
|
|
45
66
|
|
|
46
67
|
__all__ = [
|
|
47
68
|
"MobileOpsManagedAgentClient",
|
|
@@ -51,9 +72,24 @@ __all__ = [
|
|
|
51
72
|
"MobileOpsAssistantsClient",
|
|
52
73
|
"MobileOpsResourcesClient",
|
|
53
74
|
"MobileOpsResourcesError",
|
|
75
|
+
"MobileOpsVaultsClient",
|
|
76
|
+
"MobileOpsVaultsError",
|
|
77
|
+
"MobileOpsSandboxProfilesClient",
|
|
78
|
+
"MobileOpsSandboxProfilesError",
|
|
54
79
|
"ResourceFile",
|
|
55
80
|
"ResourceSummary",
|
|
56
81
|
"ResourceVersion",
|
|
82
|
+
"Vault",
|
|
83
|
+
"VaultCreateInput",
|
|
84
|
+
"VaultCredential",
|
|
85
|
+
"VaultCredentialInput",
|
|
86
|
+
"VaultCredentialUpdateInput",
|
|
87
|
+
"VaultUpdateInput",
|
|
88
|
+
"SandboxProfile",
|
|
89
|
+
"SandboxProfileCreateInput",
|
|
90
|
+
"SandboxProfileNetwork",
|
|
91
|
+
"SandboxProfileSpec",
|
|
92
|
+
"SandboxProfileUpdateInput",
|
|
57
93
|
"CapabilityBundle",
|
|
58
94
|
"MobileOpsAssistantConfig",
|
|
59
95
|
"MobileOpsAssistant",
|
|
@@ -69,10 +105,12 @@ __all__ = [
|
|
|
69
105
|
"validate_response_schema",
|
|
70
106
|
"SANDBOX_PROFILE_CONFIG_KEY",
|
|
71
107
|
"SANDBOX_PROFILE_ID_PREFIX",
|
|
108
|
+
"SYSTEM_DEFAULT_PROFILE_ID",
|
|
72
109
|
"SandboxProfileReference",
|
|
73
110
|
"validate_sandbox_profile",
|
|
74
111
|
"CREDENTIAL_VAULT_CONFIG_KEY",
|
|
75
112
|
"CREDENTIAL_VAULT_ID_PREFIX",
|
|
113
|
+
"SECRET_REDACTED_VALUE",
|
|
76
114
|
"CredentialVaultReference",
|
|
77
115
|
"validate_credential_vault",
|
|
78
116
|
]
|
|
@@ -9,6 +9,8 @@ from .auth import build_headers
|
|
|
9
9
|
from .files import MobileOpsFilesClient
|
|
10
10
|
from .assistants import MobileOpsAssistantsClient
|
|
11
11
|
from .resources import MobileOpsResourcesClient
|
|
12
|
+
from .sandbox_profiles import MobileOpsSandboxProfilesClient
|
|
13
|
+
from .vaults import MobileOpsVaultsClient
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class MobileOpsAgentClient:
|
|
@@ -62,6 +64,18 @@ class MobileOpsManagedAgentClient:
|
|
|
62
64
|
headers=headers,
|
|
63
65
|
http_client=http_client,
|
|
64
66
|
)
|
|
67
|
+
self.vaults = MobileOpsVaultsClient(
|
|
68
|
+
api_url=api_url,
|
|
69
|
+
api_key=api_key,
|
|
70
|
+
headers=headers,
|
|
71
|
+
http_client=http_client,
|
|
72
|
+
)
|
|
73
|
+
self.sandbox_profiles = MobileOpsSandboxProfilesClient(
|
|
74
|
+
api_url=api_url,
|
|
75
|
+
api_key=api_key,
|
|
76
|
+
headers=headers,
|
|
77
|
+
http_client=http_client,
|
|
78
|
+
)
|
|
65
79
|
|
|
66
80
|
async def aclose(self) -> None:
|
|
67
81
|
try:
|
|
@@ -72,7 +86,13 @@ class MobileOpsManagedAgentClient:
|
|
|
72
86
|
try:
|
|
73
87
|
await self.files.aclose()
|
|
74
88
|
finally:
|
|
75
|
-
|
|
89
|
+
try:
|
|
90
|
+
await self.resources.aclose()
|
|
91
|
+
finally:
|
|
92
|
+
try:
|
|
93
|
+
await self.vaults.aclose()
|
|
94
|
+
finally:
|
|
95
|
+
await self.sandbox_profiles.aclose()
|
|
76
96
|
|
|
77
97
|
async def __aenter__(self) -> "MobileOpsManagedAgentClient":
|
|
78
98
|
return self
|
|
@@ -12,6 +12,7 @@ from typing import Any, TypeAlias
|
|
|
12
12
|
|
|
13
13
|
SANDBOX_PROFILE_CONFIG_KEY = "haiops_sandbox_profile"
|
|
14
14
|
SANDBOX_PROFILE_ID_PREFIX = "sbp_"
|
|
15
|
+
SYSTEM_DEFAULT_PROFILE_ID = f"{SANDBOX_PROFILE_ID_PREFIX}system_default"
|
|
15
16
|
|
|
16
17
|
SandboxProfileReference: TypeAlias = dict[str, str]
|
|
17
18
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any, NotRequired, TypedDict
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .auth import build_headers
|
|
10
|
+
from .sandbox_profile import SYSTEM_DEFAULT_PROFILE_ID
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"SYSTEM_DEFAULT_PROFILE_ID",
|
|
14
|
+
"SandboxProfile",
|
|
15
|
+
"SandboxProfileCreateInput",
|
|
16
|
+
"SandboxProfileNetwork",
|
|
17
|
+
"SandboxProfileSpec",
|
|
18
|
+
"SandboxProfileUpdateInput",
|
|
19
|
+
"MobileOpsSandboxProfilesError",
|
|
20
|
+
"MobileOpsSandboxProfilesClient",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SandboxProfileNetwork(TypedDict):
|
|
25
|
+
allow: list[str]
|
|
26
|
+
allow_ports: NotRequired[list[int]]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SandboxProfileSpec(TypedDict):
|
|
30
|
+
network: SandboxProfileNetwork
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SandboxProfile(TypedDict):
|
|
34
|
+
id: str
|
|
35
|
+
name: str
|
|
36
|
+
spec: SandboxProfileSpec
|
|
37
|
+
created_at: NotRequired[str]
|
|
38
|
+
updated_at: NotRequired[str]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SandboxProfileCreateInput(TypedDict):
|
|
42
|
+
name: str
|
|
43
|
+
spec: SandboxProfileSpec
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SandboxProfileUpdateInput(TypedDict, total=False):
|
|
47
|
+
name: str
|
|
48
|
+
spec: SandboxProfileSpec
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MobileOpsSandboxProfilesError(Exception):
|
|
52
|
+
def __init__(self, response: httpx.Response, body: Any) -> None:
|
|
53
|
+
self.status_code = response.status_code
|
|
54
|
+
self.response = response
|
|
55
|
+
self.body = body
|
|
56
|
+
message = (
|
|
57
|
+
body.get("error")
|
|
58
|
+
if isinstance(body, dict) and isinstance(body.get("error"), str)
|
|
59
|
+
else (
|
|
60
|
+
"MobileOPS Sandbox Profiles request failed with status "
|
|
61
|
+
f"{response.status_code}"
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
super().__init__(message)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MobileOpsSandboxProfilesClient:
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
api_url: str,
|
|
72
|
+
api_key: str,
|
|
73
|
+
headers: Mapping[str, str] | None = None,
|
|
74
|
+
http_client: httpx.AsyncClient | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
if not api_url:
|
|
77
|
+
raise ValueError("api_url is required")
|
|
78
|
+
if not api_key:
|
|
79
|
+
raise ValueError("api_key is required")
|
|
80
|
+
self._api_url = api_url.rstrip("/")
|
|
81
|
+
self._headers = build_headers(api_key, headers)
|
|
82
|
+
self._client = http_client or httpx.AsyncClient()
|
|
83
|
+
self._owns_client = http_client is None
|
|
84
|
+
|
|
85
|
+
async def aclose(self) -> None:
|
|
86
|
+
if self._owns_client:
|
|
87
|
+
await self._client.aclose()
|
|
88
|
+
|
|
89
|
+
async def list(self) -> list[SandboxProfile]:
|
|
90
|
+
payload = await self._request_json("GET", self._profiles_url())
|
|
91
|
+
return payload["profiles"]
|
|
92
|
+
|
|
93
|
+
async def create(
|
|
94
|
+
self, *, name: str, spec: SandboxProfileSpec
|
|
95
|
+
) -> SandboxProfile:
|
|
96
|
+
payload = await self._request_json(
|
|
97
|
+
"POST", self._profiles_url(), json={"name": name, "spec": spec}
|
|
98
|
+
)
|
|
99
|
+
return payload["profile"]
|
|
100
|
+
|
|
101
|
+
async def retrieve(self, id: str) -> SandboxProfile:
|
|
102
|
+
payload = await self._request_json("GET", self._profile_url(id))
|
|
103
|
+
return payload["profile"]
|
|
104
|
+
|
|
105
|
+
async def update(
|
|
106
|
+
self,
|
|
107
|
+
id: str,
|
|
108
|
+
*,
|
|
109
|
+
name: str | None = None,
|
|
110
|
+
spec: SandboxProfileSpec | None = None,
|
|
111
|
+
) -> SandboxProfile:
|
|
112
|
+
payload_body: dict[str, Any] = {}
|
|
113
|
+
if name is not None:
|
|
114
|
+
payload_body["name"] = name
|
|
115
|
+
if spec is not None:
|
|
116
|
+
payload_body["spec"] = spec
|
|
117
|
+
payload = await self._request_json(
|
|
118
|
+
"PATCH", self._profile_url(id), json=payload_body
|
|
119
|
+
)
|
|
120
|
+
return payload["profile"]
|
|
121
|
+
|
|
122
|
+
async def delete(self, id: str) -> None:
|
|
123
|
+
await self._request("DELETE", self._profile_url(id))
|
|
124
|
+
|
|
125
|
+
def _profiles_url(self) -> str:
|
|
126
|
+
return f"{self._api_url}/context-hub/v1/sandbox-profiles"
|
|
127
|
+
|
|
128
|
+
def _profile_url(self, id: str) -> str:
|
|
129
|
+
return f"{self._profiles_url()}/{quote(id, safe='')}"
|
|
130
|
+
|
|
131
|
+
async def _request_json(
|
|
132
|
+
self, method: str, url: str, **kwargs: Any
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
return (await self._request(method, url, **kwargs)).json()
|
|
135
|
+
|
|
136
|
+
async def _request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
|
|
137
|
+
headers = {**self._headers, **kwargs.pop("headers", {})}
|
|
138
|
+
response = await self._client.request(method, url, headers=headers, **kwargs)
|
|
139
|
+
if response.is_error:
|
|
140
|
+
content_type = response.headers.get("content-type", "")
|
|
141
|
+
body: Any = response.text
|
|
142
|
+
if "application/json" in content_type:
|
|
143
|
+
try:
|
|
144
|
+
body = response.json()
|
|
145
|
+
except ValueError:
|
|
146
|
+
pass
|
|
147
|
+
raise MobileOpsSandboxProfilesError(response, body)
|
|
148
|
+
return response
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any, NotRequired, TypedDict
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .auth import build_headers
|
|
10
|
+
from .credential_vault import SECRET_REDACTED_VALUE
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"SECRET_REDACTED_VALUE",
|
|
14
|
+
"Vault",
|
|
15
|
+
"VaultCreateInput",
|
|
16
|
+
"VaultCredential",
|
|
17
|
+
"VaultCredentialInput",
|
|
18
|
+
"VaultCredentialUpdateInput",
|
|
19
|
+
"VaultUpdateInput",
|
|
20
|
+
"MobileOpsVaultsError",
|
|
21
|
+
"MobileOpsVaultsClient",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Vault(TypedDict):
|
|
26
|
+
id: str
|
|
27
|
+
name: str
|
|
28
|
+
credential_count: int
|
|
29
|
+
created_at: NotRequired[str]
|
|
30
|
+
updated_at: NotRequired[str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VaultCredential(TypedDict):
|
|
34
|
+
id: str
|
|
35
|
+
vault_id: str
|
|
36
|
+
name: str
|
|
37
|
+
value: str
|
|
38
|
+
allowed_hosts: list[str]
|
|
39
|
+
header: bool
|
|
40
|
+
body: bool
|
|
41
|
+
created_at: NotRequired[str]
|
|
42
|
+
updated_at: NotRequired[str]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class VaultCreateInput(TypedDict):
|
|
46
|
+
name: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class VaultUpdateInput(TypedDict, total=False):
|
|
50
|
+
name: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class VaultCredentialInput(TypedDict):
|
|
54
|
+
name: str
|
|
55
|
+
value: str
|
|
56
|
+
allowed_hosts: list[str]
|
|
57
|
+
header: NotRequired[bool]
|
|
58
|
+
body: NotRequired[bool]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class VaultCredentialUpdateInput(TypedDict, total=False):
|
|
62
|
+
name: str
|
|
63
|
+
value: str
|
|
64
|
+
allowed_hosts: list[str]
|
|
65
|
+
header: bool
|
|
66
|
+
body: bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MobileOpsVaultsError(Exception):
|
|
70
|
+
def __init__(self, response: httpx.Response, body: Any) -> None:
|
|
71
|
+
self.status_code = response.status_code
|
|
72
|
+
self.response = response
|
|
73
|
+
self.body = body
|
|
74
|
+
message = (
|
|
75
|
+
body.get("error")
|
|
76
|
+
if isinstance(body, dict) and isinstance(body.get("error"), str)
|
|
77
|
+
else f"MobileOPS Vaults request failed with status {response.status_code}"
|
|
78
|
+
)
|
|
79
|
+
super().__init__(message)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MobileOpsVaultCredentialsClient:
|
|
83
|
+
def __init__(self, vaults: "MobileOpsVaultsClient") -> None:
|
|
84
|
+
self._vaults = vaults
|
|
85
|
+
|
|
86
|
+
async def list(self, vault_id: str) -> list[VaultCredential]:
|
|
87
|
+
payload = await self._vaults._request_json(
|
|
88
|
+
"GET", self._vaults._credentials_url(vault_id)
|
|
89
|
+
)
|
|
90
|
+
return payload["credentials"]
|
|
91
|
+
|
|
92
|
+
async def create(
|
|
93
|
+
self,
|
|
94
|
+
vault_id: str,
|
|
95
|
+
*,
|
|
96
|
+
name: str,
|
|
97
|
+
value: str,
|
|
98
|
+
allowed_hosts: list[str],
|
|
99
|
+
header: bool | None = None,
|
|
100
|
+
body: bool | None = None,
|
|
101
|
+
) -> VaultCredential:
|
|
102
|
+
payload_body: dict[str, Any] = {
|
|
103
|
+
"name": name,
|
|
104
|
+
"value": value,
|
|
105
|
+
"allowed_hosts": allowed_hosts,
|
|
106
|
+
}
|
|
107
|
+
if header is not None:
|
|
108
|
+
payload_body["header"] = header
|
|
109
|
+
if body is not None:
|
|
110
|
+
payload_body["body"] = body
|
|
111
|
+
payload = await self._vaults._request_json(
|
|
112
|
+
"POST",
|
|
113
|
+
self._vaults._credentials_url(vault_id),
|
|
114
|
+
json=payload_body,
|
|
115
|
+
)
|
|
116
|
+
return payload["credential"]
|
|
117
|
+
|
|
118
|
+
async def update(
|
|
119
|
+
self,
|
|
120
|
+
vault_id: str,
|
|
121
|
+
credential_id: str,
|
|
122
|
+
*,
|
|
123
|
+
name: str | None = None,
|
|
124
|
+
value: str | None = None,
|
|
125
|
+
allowed_hosts: list[str] | None = None,
|
|
126
|
+
header: bool | None = None,
|
|
127
|
+
body: bool | None = None,
|
|
128
|
+
) -> VaultCredential:
|
|
129
|
+
payload_body: dict[str, Any] = {}
|
|
130
|
+
if name is not None:
|
|
131
|
+
payload_body["name"] = name
|
|
132
|
+
if value is not None:
|
|
133
|
+
payload_body["value"] = value
|
|
134
|
+
if allowed_hosts is not None:
|
|
135
|
+
payload_body["allowed_hosts"] = allowed_hosts
|
|
136
|
+
if header is not None:
|
|
137
|
+
payload_body["header"] = header
|
|
138
|
+
if body is not None:
|
|
139
|
+
payload_body["body"] = body
|
|
140
|
+
payload = await self._vaults._request_json(
|
|
141
|
+
"PATCH",
|
|
142
|
+
self._vaults._credential_url(vault_id, credential_id),
|
|
143
|
+
json=payload_body,
|
|
144
|
+
)
|
|
145
|
+
return payload["credential"]
|
|
146
|
+
|
|
147
|
+
async def delete(self, vault_id: str, credential_id: str) -> None:
|
|
148
|
+
await self._vaults._request(
|
|
149
|
+
"DELETE", self._vaults._credential_url(vault_id, credential_id)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class MobileOpsVaultsClient:
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
api_url: str,
|
|
158
|
+
api_key: str,
|
|
159
|
+
headers: Mapping[str, str] | None = None,
|
|
160
|
+
http_client: httpx.AsyncClient | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
if not api_url:
|
|
163
|
+
raise ValueError("api_url is required")
|
|
164
|
+
if not api_key:
|
|
165
|
+
raise ValueError("api_key is required")
|
|
166
|
+
self._api_url = api_url.rstrip("/")
|
|
167
|
+
self._headers = build_headers(api_key, headers)
|
|
168
|
+
self._client = http_client or httpx.AsyncClient()
|
|
169
|
+
self._owns_client = http_client is None
|
|
170
|
+
self.credentials = MobileOpsVaultCredentialsClient(self)
|
|
171
|
+
|
|
172
|
+
async def aclose(self) -> None:
|
|
173
|
+
if self._owns_client:
|
|
174
|
+
await self._client.aclose()
|
|
175
|
+
|
|
176
|
+
async def list(self) -> list[Vault]:
|
|
177
|
+
payload = await self._request_json("GET", self._vaults_url())
|
|
178
|
+
return payload["vaults"]
|
|
179
|
+
|
|
180
|
+
async def create(self, *, name: str) -> Vault:
|
|
181
|
+
payload = await self._request_json(
|
|
182
|
+
"POST", self._vaults_url(), json={"name": name}
|
|
183
|
+
)
|
|
184
|
+
return payload["vault"]
|
|
185
|
+
|
|
186
|
+
async def retrieve(self, id: str) -> Vault:
|
|
187
|
+
payload = await self._request_json("GET", self._vault_url(id))
|
|
188
|
+
return payload["vault"]
|
|
189
|
+
|
|
190
|
+
async def update(self, id: str, *, name: str | None = None) -> Vault:
|
|
191
|
+
payload_body: dict[str, Any] = {}
|
|
192
|
+
if name is not None:
|
|
193
|
+
payload_body["name"] = name
|
|
194
|
+
payload = await self._request_json(
|
|
195
|
+
"PATCH", self._vault_url(id), json=payload_body
|
|
196
|
+
)
|
|
197
|
+
return payload["vault"]
|
|
198
|
+
|
|
199
|
+
async def delete(self, id: str) -> None:
|
|
200
|
+
await self._request("DELETE", self._vault_url(id))
|
|
201
|
+
|
|
202
|
+
def _vaults_url(self) -> str:
|
|
203
|
+
return f"{self._api_url}/context-hub/v1/vaults"
|
|
204
|
+
|
|
205
|
+
def _vault_url(self, id: str) -> str:
|
|
206
|
+
return f"{self._vaults_url()}/{quote(id, safe='')}"
|
|
207
|
+
|
|
208
|
+
def _credentials_url(self, vault_id: str) -> str:
|
|
209
|
+
return f"{self._vault_url(vault_id)}/credentials"
|
|
210
|
+
|
|
211
|
+
def _credential_url(self, vault_id: str, credential_id: str) -> str:
|
|
212
|
+
return f"{self._credentials_url(vault_id)}/{quote(credential_id, safe='')}"
|
|
213
|
+
|
|
214
|
+
async def _request_json(
|
|
215
|
+
self, method: str, url: str, **kwargs: Any
|
|
216
|
+
) -> dict[str, Any]:
|
|
217
|
+
return (await self._request(method, url, **kwargs)).json()
|
|
218
|
+
|
|
219
|
+
async def _request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
|
|
220
|
+
headers = {**self._headers, **kwargs.pop("headers", {})}
|
|
221
|
+
response = await self._client.request(method, url, headers=headers, **kwargs)
|
|
222
|
+
if response.is_error:
|
|
223
|
+
content_type = response.headers.get("content-type", "")
|
|
224
|
+
body: Any = response.text
|
|
225
|
+
if "application/json" in content_type:
|
|
226
|
+
try:
|
|
227
|
+
body = response.json()
|
|
228
|
+
except ValueError:
|
|
229
|
+
pass
|
|
230
|
+
raise MobileOpsVaultsError(response, body)
|
|
231
|
+
return response
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mobileops-managed-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -18,11 +18,13 @@ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
|
18
18
|
|
|
19
19
|
Server-side Python SDK for the MobileOPS Managed Agent API.
|
|
20
20
|
|
|
21
|
-
The SDK exposes
|
|
21
|
+
The SDK exposes five surfaces:
|
|
22
22
|
|
|
23
23
|
- `client.agent` wraps the LangGraph runtime client and exposes the supported Assistant CRUD methods at `client.agent.assistants`.
|
|
24
24
|
- `client.files` exposes MobileOPS Sandbox Workspace file routes.
|
|
25
25
|
- `client.resources` manages Context Hub resources, versions, and latest-version files.
|
|
26
|
+
- `client.vaults` manages credential vaults and their write-only credentials.
|
|
27
|
+
- `client.sandbox_profiles` manages sandbox profiles (运行环境).
|
|
26
28
|
|
|
27
29
|
Assistant create and update retain LangGraph's ordinary arguments, while
|
|
28
30
|
persisted configuration is exposed through product fields. Pass
|
|
@@ -37,6 +39,8 @@ therefore only affects Runs started on **new** Threads; existing Threads keep
|
|
|
37
39
|
the schema captured at their creation.
|
|
38
40
|
|
|
39
41
|
```py
|
|
42
|
+
import os
|
|
43
|
+
|
|
40
44
|
from mobileops_managed_agent import MobileOpsManagedAgentClient
|
|
41
45
|
|
|
42
46
|
client = MobileOpsManagedAgentClient(api_url=api_url, api_key=api_key)
|
|
@@ -45,12 +49,25 @@ thread = await client.agent.threads.create()
|
|
|
45
49
|
await client.agent.runs.create(thread["thread_id"], assistant_id, input=input)
|
|
46
50
|
|
|
47
51
|
skill = await client.resources.create("skills", zip_bytes) # slug inferred
|
|
52
|
+
profile = await client.sandbox_profiles.create(
|
|
53
|
+
name="prod",
|
|
54
|
+
spec={"network": {"allow": ["*.example.com"]}},
|
|
55
|
+
)
|
|
56
|
+
vault = await client.vaults.create(name="prod")
|
|
57
|
+
await client.vaults.credentials.create(
|
|
58
|
+
vault["id"],
|
|
59
|
+
name="GRAFANA_TOKEN",
|
|
60
|
+
value=os.environ["GRAFANA_TOKEN"],
|
|
61
|
+
allowed_hosts=["grafana.example.com"],
|
|
62
|
+
)
|
|
48
63
|
assistant = await client.agent.assistants.create(
|
|
49
64
|
name="Operations agent",
|
|
50
65
|
skills=[{
|
|
51
66
|
"ref": skill["ref"], # e.g. incident-response@sha256-…
|
|
52
67
|
"target_path": "/workspace/skills", # zip already contains {slug}/
|
|
53
68
|
}],
|
|
69
|
+
sandbox_profile=profile["id"],
|
|
70
|
+
credential_vault=vault["id"],
|
|
54
71
|
)
|
|
55
72
|
|
|
56
73
|
await client.resources.versions.create("skills", skill["slug"], next_zip_bytes)
|
|
@@ -12,6 +12,8 @@ src/mobileops_managed_agent/files.py
|
|
|
12
12
|
src/mobileops_managed_agent/resources.py
|
|
13
13
|
src/mobileops_managed_agent/response_schema.py
|
|
14
14
|
src/mobileops_managed_agent/sandbox_profile.py
|
|
15
|
+
src/mobileops_managed_agent/sandbox_profiles.py
|
|
16
|
+
src/mobileops_managed_agent/vaults.py
|
|
15
17
|
src/mobileops_managed_agent.egg-info/PKG-INFO
|
|
16
18
|
src/mobileops_managed_agent.egg-info/SOURCES.txt
|
|
17
19
|
src/mobileops_managed_agent.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{mobileops_managed_agent-0.2.2 → mobileops_managed_agent-0.3.0}/src/mobileops_managed_agent/files.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|