agentstack-cli 0.6.0rc3__tar.gz → 0.6.1rc1__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.
- agentstack_cli-0.6.1rc1/PKG-INFO +26 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/pyproject.toml +21 -20
- agentstack_cli-0.6.1rc1/src/agentstack_cli/auth_manager.py +294 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/agent.py +43 -5
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/build.py +1 -2
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/connector.py +5 -4
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/model.py +2 -1
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/platform/__init__.py +14 -16
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/platform/base_driver.py +101 -39
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/platform/lima_driver.py +2 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/platform/wsl_driver.py +5 -8
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/self.py +2 -2
- agentstack_cli-0.6.1rc1/src/agentstack_cli/commands/server.py +366 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/user.py +1 -1
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/configuration.py +21 -1
- agentstack_cli-0.6.1rc1/src/agentstack_cli/data/helm-chart.tgz +0 -0
- agentstack_cli-0.6.1rc1/src/agentstack_cli/server_utils.py +40 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/utils.py +18 -34
- agentstack_cli-0.6.0rc3/PKG-INFO +0 -26
- agentstack_cli-0.6.0rc3/src/agentstack_cli/auth_manager.py +0 -242
- agentstack_cli-0.6.0rc3/src/agentstack_cli/commands/server.py +0 -315
- agentstack_cli-0.6.0rc3/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/README.md +0 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/__init__.py +0 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/api.py +0 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/async_typer.py +0 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/__init__.py +0 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/console.py +0 -0
- {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/data/.gitignore +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: agentstack-cli
|
|
3
|
+
Version: 0.6.1rc1
|
|
4
|
+
Summary: Agent Stack CLI
|
|
5
|
+
Author: IBM Corp.
|
|
6
|
+
Requires-Dist: anyio>=4.12.1
|
|
7
|
+
Requires-Dist: pydantic>=2.12.5
|
|
8
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
9
|
+
Requires-Dist: requests>=2.32.5
|
|
10
|
+
Requires-Dist: jsonschema>=4.26.0
|
|
11
|
+
Requires-Dist: jsf>=0.11.2
|
|
12
|
+
Requires-Dist: gnureadline>=8.3.3 ; sys_platform != 'win32'
|
|
13
|
+
Requires-Dist: prompt-toolkit>=3.0.52
|
|
14
|
+
Requires-Dist: inquirerpy>=0.3.4
|
|
15
|
+
Requires-Dist: psutil>=7.2.2
|
|
16
|
+
Requires-Dist: a2a-sdk
|
|
17
|
+
Requires-Dist: tenacity>=9.1.2
|
|
18
|
+
Requires-Dist: typer>=0.21.1
|
|
19
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
20
|
+
Requires-Dist: agentstack-sdk
|
|
21
|
+
Requires-Dist: authlib>=1.6.6
|
|
22
|
+
Requires-Dist: openai>=2.16.0
|
|
23
|
+
Requires-Python: >=3.13, <3.14
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Agent Stack CLI
|
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentstack-cli"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.1-rc1"
|
|
4
4
|
description = "Agent Stack CLI"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "IBM Corp." }]
|
|
7
7
|
requires-python = ">=3.13,<3.14"
|
|
8
8
|
dependencies = [
|
|
9
|
-
"anyio
|
|
10
|
-
"pydantic
|
|
11
|
-
"pydantic-settings
|
|
12
|
-
"requests
|
|
13
|
-
"jsonschema
|
|
14
|
-
"jsf
|
|
15
|
-
'gnureadline
|
|
16
|
-
"prompt-toolkit
|
|
17
|
-
"inquirerpy
|
|
18
|
-
"psutil
|
|
9
|
+
"anyio>=4.12.1",
|
|
10
|
+
"pydantic>=2.12.5",
|
|
11
|
+
"pydantic-settings>=2.12.0",
|
|
12
|
+
"requests>=2.32.5",
|
|
13
|
+
"jsonschema>=4.26.0",
|
|
14
|
+
"jsf>=0.11.2",
|
|
15
|
+
'gnureadline>=8.3.3; sys_platform != "win32"',
|
|
16
|
+
"prompt-toolkit>=3.0.52",
|
|
17
|
+
"inquirerpy>=0.3.4",
|
|
18
|
+
"psutil>=7.2.2",
|
|
19
19
|
"a2a-sdk", # version determined by agentstack-sdk
|
|
20
|
-
"tenacity
|
|
21
|
-
"typer
|
|
22
|
-
"pyyaml
|
|
20
|
+
"tenacity>=9.1.2",
|
|
21
|
+
"typer>=0.21.1",
|
|
22
|
+
"pyyaml>=6.0.3",
|
|
23
23
|
"agentstack-sdk",
|
|
24
|
-
"authlib
|
|
25
|
-
"openai
|
|
24
|
+
"authlib>=1.6.6",
|
|
25
|
+
"openai>=2.16.0",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[dependency-groups]
|
|
29
29
|
dev = [
|
|
30
|
-
"pyright>=1.1.
|
|
31
|
-
"pytest>=
|
|
32
|
-
"ruff>=0.
|
|
33
|
-
"wheel>=0.
|
|
30
|
+
"pyright>=1.1.407", # note: pyright 1.1.408 has a bug with multiple-inheritance
|
|
31
|
+
"pytest>=9.0.2",
|
|
32
|
+
"ruff>=0.14.14",
|
|
33
|
+
"wheel>=0.46.3",
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[tool.uv.sources]
|
|
@@ -78,3 +78,4 @@ force-exclude = true
|
|
|
78
78
|
ignore = ["tests/**", "examples/cli.py"]
|
|
79
79
|
venvPath = "."
|
|
80
80
|
venv = ".venv"
|
|
81
|
+
reportUnusedCallResult = false
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import pathlib
|
|
4
|
+
import time
|
|
5
|
+
import typing
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from authlib.common.errors import AuthlibBaseError
|
|
11
|
+
from authlib.integrations.httpx_client import AsyncOAuth2Client
|
|
12
|
+
from authlib.oauth2.rfc6749.errors import InvalidGrantError, OAuth2Error
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
TOKEN_EXPIRY_LEEWAY = 60 # seconds
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthToken(BaseModel):
|
|
19
|
+
access_token: str
|
|
20
|
+
token_type: str = "Bearer"
|
|
21
|
+
expires_in: int | None = None
|
|
22
|
+
expires_at: int | None = None
|
|
23
|
+
refresh_token: str | None = None
|
|
24
|
+
scope: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuthServer(BaseModel):
|
|
28
|
+
client_id: str = "df82a687-d647-4247-838b-7080d7d83f6c" # Backwards compatibility default
|
|
29
|
+
client_secret: str | None = None
|
|
30
|
+
token: AuthToken | None = None
|
|
31
|
+
registration_token: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Server(BaseModel):
|
|
35
|
+
authorization_servers: dict[str, AuthServer] = Field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Auth(BaseModel):
|
|
39
|
+
version: typing.Literal[1] = 1
|
|
40
|
+
servers: defaultdict[str, typing.Annotated[Server, Field(default_factory=Server)]] = Field(
|
|
41
|
+
default_factory=lambda: defaultdict(Server)
|
|
42
|
+
)
|
|
43
|
+
active_server: str | None = None
|
|
44
|
+
active_auth_server: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@typing.final
|
|
48
|
+
class AuthManager:
|
|
49
|
+
def __init__(self, config_path: pathlib.Path):
|
|
50
|
+
self._auth_path = config_path
|
|
51
|
+
self._auth = self._load()
|
|
52
|
+
self._oidc_cache: dict[str, dict[str, Any]] = {}
|
|
53
|
+
|
|
54
|
+
def _load(self) -> Auth:
|
|
55
|
+
if not self._auth_path.exists():
|
|
56
|
+
return Auth()
|
|
57
|
+
return Auth.model_validate_json(self._auth_path.read_bytes())
|
|
58
|
+
|
|
59
|
+
def _save(self) -> None:
|
|
60
|
+
self._auth_path.write_text(self._auth.model_dump_json(indent=2))
|
|
61
|
+
|
|
62
|
+
async def _get_oidc_metadata(self, auth_server: str) -> dict[str, Any]:
|
|
63
|
+
"""Fetch and cache OIDC metadata."""
|
|
64
|
+
if auth_server in self._oidc_cache:
|
|
65
|
+
return self._oidc_cache[auth_server]
|
|
66
|
+
|
|
67
|
+
async with httpx.AsyncClient() as client:
|
|
68
|
+
try:
|
|
69
|
+
resp = await client.get(f"{auth_server}/.well-known/openid-configuration")
|
|
70
|
+
resp.raise_for_status()
|
|
71
|
+
metadata = resp.json()
|
|
72
|
+
self._oidc_cache[auth_server] = metadata
|
|
73
|
+
return metadata
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise RuntimeError(f"OIDC discovery failed: {e}") from e
|
|
76
|
+
|
|
77
|
+
def _create_token_update_callback(self, server: str, auth_server: str):
|
|
78
|
+
"""Create a callback that saves tokens when they're refreshed."""
|
|
79
|
+
|
|
80
|
+
def update_token(token: dict[str, Any]):
|
|
81
|
+
# Authlib calls this automatically when tokens are refreshed
|
|
82
|
+
# kwargs may include refresh_token and access_token but we don't need them
|
|
83
|
+
auth_config = self._auth.servers[server].authorization_servers[auth_server]
|
|
84
|
+
self.save_auth_info(
|
|
85
|
+
server=server,
|
|
86
|
+
auth_server=auth_server,
|
|
87
|
+
client_id=auth_config.client_id,
|
|
88
|
+
client_secret=auth_config.client_secret,
|
|
89
|
+
token=token,
|
|
90
|
+
registration_token=auth_config.registration_token,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return update_token
|
|
94
|
+
|
|
95
|
+
async def _get_oauth_client(self, server: str, auth_server: str) -> AsyncOAuth2Client:
|
|
96
|
+
"""Create an OAuth2 client configured with current credentials."""
|
|
97
|
+
auth_config = self._auth.servers[server].authorization_servers[auth_server]
|
|
98
|
+
|
|
99
|
+
if not auth_config or not auth_config.token:
|
|
100
|
+
raise ValueError(f"No token found for {auth_server}")
|
|
101
|
+
|
|
102
|
+
metadata = await self._get_oidc_metadata(auth_server)
|
|
103
|
+
|
|
104
|
+
# Convert AuthToken to dict format authlib expects
|
|
105
|
+
token_dict = auth_config.token.model_dump(exclude_none=True)
|
|
106
|
+
|
|
107
|
+
client = AsyncOAuth2Client(
|
|
108
|
+
client_id=auth_config.client_id,
|
|
109
|
+
client_secret=auth_config.client_secret,
|
|
110
|
+
token_endpoint=metadata["token_endpoint"],
|
|
111
|
+
token=token_dict,
|
|
112
|
+
scope=token_dict.get("scope"),
|
|
113
|
+
update_token=self._create_token_update_callback(server, auth_server),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return client
|
|
117
|
+
|
|
118
|
+
def save_auth_info(
|
|
119
|
+
self,
|
|
120
|
+
server: str,
|
|
121
|
+
auth_server: str | None = None,
|
|
122
|
+
client_id: str | None = None,
|
|
123
|
+
client_secret: str | None = None,
|
|
124
|
+
token: dict[str, Any] | None = None,
|
|
125
|
+
registration_token: str | None = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
if auth_server is not None and client_id is not None and token is not None:
|
|
128
|
+
if token["access_token"] and token.get("expires_in") is not None:
|
|
129
|
+
usetimestamp = int(time.time()) + int(token["expires_in"])
|
|
130
|
+
token["expires_at"] = usetimestamp
|
|
131
|
+
self._auth.servers[server].authorization_servers[auth_server] = AuthServer(
|
|
132
|
+
client_id=client_id,
|
|
133
|
+
client_secret=client_secret,
|
|
134
|
+
token=AuthToken(**token),
|
|
135
|
+
registration_token=registration_token,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
self._auth.servers[server] # touch
|
|
139
|
+
self._save()
|
|
140
|
+
|
|
141
|
+
async def exchange_refresh_token(self, auth_server: str, token: AuthToken) -> dict[str, Any] | None:
|
|
142
|
+
"""
|
|
143
|
+
Exchange a refresh token for a new access token using authlib.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
InvalidGrantError: If the refresh token is invalid or expired (4xx auth errors)
|
|
147
|
+
OAuth2Error: For other OAuth2 protocol errors
|
|
148
|
+
RuntimeError: For network errors or OIDC discovery failures
|
|
149
|
+
"""
|
|
150
|
+
if not self._auth.active_server:
|
|
151
|
+
raise ValueError("No active server configured")
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
metadata = await self._get_oidc_metadata(auth_server)
|
|
155
|
+
token_endpoint = metadata["token_endpoint"]
|
|
156
|
+
|
|
157
|
+
async with await self._get_oauth_client(self._auth.active_server, auth_server) as client:
|
|
158
|
+
# Authlib's fetch_token with refresh_token grant automatically handles the refresh
|
|
159
|
+
# and calls update_token callback to save the new token
|
|
160
|
+
new_token = await client.fetch_token(
|
|
161
|
+
url=token_endpoint,
|
|
162
|
+
grant_type="refresh_token",
|
|
163
|
+
refresh_token=token.refresh_token,
|
|
164
|
+
)
|
|
165
|
+
return new_token
|
|
166
|
+
except InvalidGrantError as e:
|
|
167
|
+
# 400-level OAuth errors: invalid/expired refresh token
|
|
168
|
+
raise InvalidGrantError(
|
|
169
|
+
description=f"Token refresh failed - invalid or expired refresh token: {e.description}"
|
|
170
|
+
) from e
|
|
171
|
+
except OAuth2Error as e:
|
|
172
|
+
# Other OAuth2 protocol errors
|
|
173
|
+
raise OAuth2Error(description=f"OAuth2 error during token refresh: {e.description}") from e
|
|
174
|
+
except AuthlibBaseError as e:
|
|
175
|
+
# Other authlib errors
|
|
176
|
+
raise RuntimeError(f"Token refresh failed: {e}") from e
|
|
177
|
+
except Exception as e:
|
|
178
|
+
# Network errors, OIDC discovery failures, etc.
|
|
179
|
+
raise RuntimeError(f"Failed to refresh token: {e}") from e
|
|
180
|
+
|
|
181
|
+
async def load_auth_token(self) -> str | None:
|
|
182
|
+
"""
|
|
183
|
+
Load and refresh auth token if needed using authlib.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Access token string, or None if no auth configured
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
InvalidGrantError: If token is expired and refresh fails due to auth issues (4xx)
|
|
190
|
+
OAuth2Error: For other OAuth2 protocol errors
|
|
191
|
+
RuntimeError: For network or other errors
|
|
192
|
+
"""
|
|
193
|
+
active_res = self._auth.active_server
|
|
194
|
+
active_auth_server = self._auth.active_auth_server
|
|
195
|
+
if not active_res or not active_auth_server:
|
|
196
|
+
return None
|
|
197
|
+
server = self._auth.servers.get(active_res)
|
|
198
|
+
if not server:
|
|
199
|
+
return None
|
|
200
|
+
auth_server = server.authorization_servers.get(active_auth_server)
|
|
201
|
+
if not auth_server or not auth_server.token:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
if (auth_server.token.expires_at or 0) - TOKEN_EXPIRY_LEEWAY < time.time():
|
|
205
|
+
# Token expired, try to refresh - this may raise TokenRefreshError
|
|
206
|
+
new_token = await self.exchange_refresh_token(active_auth_server, auth_server.token)
|
|
207
|
+
if new_token:
|
|
208
|
+
return new_token["access_token"]
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
return auth_server.token.access_token
|
|
212
|
+
|
|
213
|
+
async def deregister_client(self, auth_server: str, client_id: str | None, registration_token: str | None) -> None:
|
|
214
|
+
"""Deregister a dynamically registered OAuth2 client."""
|
|
215
|
+
if not client_id or not registration_token:
|
|
216
|
+
return # Nothing to deregister
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
metadata = await self._get_oidc_metadata(auth_server)
|
|
220
|
+
registration_endpoint = metadata.get("registration_endpoint")
|
|
221
|
+
|
|
222
|
+
if not registration_endpoint:
|
|
223
|
+
raise RuntimeError("Registration endpoint not found in OIDC metadata")
|
|
224
|
+
|
|
225
|
+
async with AsyncOAuth2Client() as client:
|
|
226
|
+
headers = {"Authorization": f"Bearer {registration_token}"}
|
|
227
|
+
resp = await client.delete(f"{registration_endpoint}/{client_id}", headers=headers)
|
|
228
|
+
resp.raise_for_status()
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
raise RuntimeError(f"Dynamic client de-registration failed: {e}") from e
|
|
232
|
+
|
|
233
|
+
async def clear_auth_token(self, all: bool = False) -> None:
|
|
234
|
+
if all:
|
|
235
|
+
for server in self._auth.servers:
|
|
236
|
+
for auth_server in self._auth.servers[server].authorization_servers:
|
|
237
|
+
auth_config = self._auth.servers[server].authorization_servers[auth_server]
|
|
238
|
+
await self.deregister_client(
|
|
239
|
+
auth_server,
|
|
240
|
+
auth_config.client_id,
|
|
241
|
+
auth_config.registration_token,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
self._auth.servers = defaultdict(Server)
|
|
245
|
+
else:
|
|
246
|
+
if self._auth.active_server and self._auth.active_auth_server:
|
|
247
|
+
auth_config = self._auth.servers[self._auth.active_server].authorization_servers[
|
|
248
|
+
self._auth.active_auth_server
|
|
249
|
+
]
|
|
250
|
+
await self.deregister_client(
|
|
251
|
+
self._auth.active_auth_server,
|
|
252
|
+
auth_config.client_id,
|
|
253
|
+
auth_config.registration_token,
|
|
254
|
+
)
|
|
255
|
+
del self._auth.servers[self._auth.active_server].authorization_servers[self._auth.active_auth_server]
|
|
256
|
+
|
|
257
|
+
if self._auth.active_server and not self._auth.servers[self._auth.active_server].authorization_servers:
|
|
258
|
+
del self._auth.servers[self._auth.active_server]
|
|
259
|
+
|
|
260
|
+
self._auth.active_server = None
|
|
261
|
+
self._auth.active_auth_server = None
|
|
262
|
+
self._save()
|
|
263
|
+
|
|
264
|
+
def get_server(self, server: str) -> Server | None:
|
|
265
|
+
return self._auth.servers.get(server)
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def servers(self) -> list[str]:
|
|
269
|
+
return list(self._auth.servers.keys())
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def active_server(self) -> str | None:
|
|
273
|
+
return self._auth.active_server
|
|
274
|
+
|
|
275
|
+
@active_server.setter
|
|
276
|
+
def active_server(self, server: str | None) -> None:
|
|
277
|
+
if server is not None and server not in self._auth.servers:
|
|
278
|
+
raise ValueError(f"Server {server} not found")
|
|
279
|
+
self._auth.active_server = server
|
|
280
|
+
self._save()
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def active_auth_server(self) -> str | None:
|
|
284
|
+
return self._auth.active_auth_server
|
|
285
|
+
|
|
286
|
+
@active_auth_server.setter
|
|
287
|
+
def active_auth_server(self, auth_server: str | None) -> None:
|
|
288
|
+
if auth_server is not None and (
|
|
289
|
+
self._auth.active_server not in self._auth.servers
|
|
290
|
+
or auth_server not in self._auth.servers[self._auth.active_server].authorization_servers
|
|
291
|
+
):
|
|
292
|
+
raise ValueError(f"Auth server {auth_server} not found in active server")
|
|
293
|
+
self._auth.active_auth_server = auth_server
|
|
294
|
+
self._save()
|
|
@@ -112,9 +112,8 @@ from rich.table import Column
|
|
|
112
112
|
|
|
113
113
|
from agentstack_cli.api import a2a_client
|
|
114
114
|
from agentstack_cli.async_typer import AsyncTyper, console, create_table, err_console
|
|
115
|
+
from agentstack_cli.server_utils import announce_server_action, confirm_server_action
|
|
115
116
|
from agentstack_cli.utils import (
|
|
116
|
-
announce_server_action,
|
|
117
|
-
confirm_server_action,
|
|
118
117
|
generate_schema_example,
|
|
119
118
|
is_github_url,
|
|
120
119
|
parse_env_var,
|
|
@@ -181,6 +180,36 @@ processing_messages = [
|
|
|
181
180
|
|
|
182
181
|
configuration = Configuration()
|
|
183
182
|
|
|
183
|
+
DISCOVERY_TIMEOUT_SEC = 180
|
|
184
|
+
DISCOVERY_POLL_INTERVAL_SEC = 2
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def _discover_agent_card(docker_image: str) -> AgentCard:
|
|
188
|
+
from agentstack_sdk.platform.provider_discovery import DiscoveryState, ProviderDiscovery
|
|
189
|
+
|
|
190
|
+
console.info("Image missing agent card label, starting discovery...")
|
|
191
|
+
|
|
192
|
+
async with configuration.use_platform_client():
|
|
193
|
+
with status("Creating discovery task"):
|
|
194
|
+
discovery = await ProviderDiscovery.create(docker_image=docker_image)
|
|
195
|
+
|
|
196
|
+
start = asyncio.get_event_loop().time()
|
|
197
|
+
with status("Discovering agent card (this may take a while)"):
|
|
198
|
+
while discovery.status in (DiscoveryState.PENDING, DiscoveryState.IN_PROGRESS):
|
|
199
|
+
if asyncio.get_event_loop().time() - start > DISCOVERY_TIMEOUT_SEC:
|
|
200
|
+
raise RuntimeError("Discovery timed out after 3 minutes")
|
|
201
|
+
await asyncio.sleep(DISCOVERY_POLL_INTERVAL_SEC)
|
|
202
|
+
await discovery.get()
|
|
203
|
+
|
|
204
|
+
if discovery.status == DiscoveryState.FAILED:
|
|
205
|
+
raise RuntimeError(f"Discovery failed: {discovery.error_message}")
|
|
206
|
+
|
|
207
|
+
card = discovery.agent_card
|
|
208
|
+
if not card:
|
|
209
|
+
raise RuntimeError("Discovery completed but no agent card was returned")
|
|
210
|
+
|
|
211
|
+
return card
|
|
212
|
+
|
|
184
213
|
|
|
185
214
|
@app.command("add")
|
|
186
215
|
async def add_agent(
|
|
@@ -257,9 +286,18 @@ async def add_agent(
|
|
|
257
286
|
if dockerfile:
|
|
258
287
|
raise ValueError("Dockerfile can be specified only if location is a GitHub url")
|
|
259
288
|
console.info(f"Assuming public docker image or network address, attempting to add {location}")
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
289
|
+
try:
|
|
290
|
+
with status("Registering agent to platform"):
|
|
291
|
+
async with configuration.use_platform_client():
|
|
292
|
+
await Provider.create(location=location)
|
|
293
|
+
except httpx.HTTPStatusError as e:
|
|
294
|
+
if e.response.status_code == 422:
|
|
295
|
+
agent_card = await _discover_agent_card(location)
|
|
296
|
+
with status("Registering agent with discovered card"):
|
|
297
|
+
async with configuration.use_platform_client():
|
|
298
|
+
await Provider.create(location=location, agent_card=agent_card)
|
|
299
|
+
else:
|
|
300
|
+
raise
|
|
263
301
|
console.success(f"Agent [bold]{location}[/bold] added to platform")
|
|
264
302
|
await list_agents()
|
|
265
303
|
|
|
@@ -25,10 +25,9 @@ from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_delay, w
|
|
|
25
25
|
|
|
26
26
|
from agentstack_cli.async_typer import AsyncTyper
|
|
27
27
|
from agentstack_cli.console import console, err_console
|
|
28
|
+
from agentstack_cli.server_utils import announce_server_action, confirm_server_action
|
|
28
29
|
from agentstack_cli.utils import (
|
|
29
|
-
announce_server_action,
|
|
30
30
|
capture_output,
|
|
31
|
-
confirm_server_action,
|
|
32
31
|
extract_messages,
|
|
33
32
|
print_log,
|
|
34
33
|
run_command,
|
{agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/connector.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import asyncio
|
|
4
|
+
import sys
|
|
4
5
|
import typing
|
|
5
6
|
|
|
6
7
|
import pydantic
|
|
@@ -14,7 +15,7 @@ from agentstack_cli import configuration
|
|
|
14
15
|
from agentstack_cli.async_typer import AsyncTyper
|
|
15
16
|
from agentstack_cli.configuration import Configuration
|
|
16
17
|
from agentstack_cli.console import console
|
|
17
|
-
from agentstack_cli.
|
|
18
|
+
from agentstack_cli.server_utils import (
|
|
18
19
|
announce_server_action,
|
|
19
20
|
confirm_server_action,
|
|
20
21
|
)
|
|
@@ -99,7 +100,7 @@ async def remove_connector(
|
|
|
99
100
|
console.error(
|
|
100
101
|
"[red]Cannot specify both --all and a search path. Use --all to remove all connectors, or provide a search path for specific connectors.[/red]"
|
|
101
102
|
)
|
|
102
|
-
|
|
103
|
+
sys.exit(1)
|
|
103
104
|
|
|
104
105
|
async with configuration.use_platform_client():
|
|
105
106
|
connectors_list = await Connector.list()
|
|
@@ -191,7 +192,7 @@ async def select_connector(search_path: str) -> Connector | None:
|
|
|
191
192
|
except ValueError as e:
|
|
192
193
|
console.error(e.__str__())
|
|
193
194
|
console.hint("Please refine your input to match exactly one connector id or url.")
|
|
194
|
-
|
|
195
|
+
sys.exit(1)
|
|
195
196
|
|
|
196
197
|
|
|
197
198
|
@app.command("get")
|
|
@@ -258,7 +259,7 @@ async def disconnect(
|
|
|
258
259
|
console.error(
|
|
259
260
|
"[red]Cannot specify both --all and a search path. Use --all to remove all connectors, or provide a search path for specific connectors.[/red]"
|
|
260
261
|
)
|
|
261
|
-
|
|
262
|
+
sys.exit(1)
|
|
262
263
|
|
|
263
264
|
async with configuration.use_platform_client():
|
|
264
265
|
connectors_list = await Connector.list()
|
|
@@ -25,7 +25,8 @@ from rich.table import Column
|
|
|
25
25
|
from agentstack_cli.api import openai_client
|
|
26
26
|
from agentstack_cli.async_typer import AsyncTyper, console, create_table
|
|
27
27
|
from agentstack_cli.configuration import Configuration
|
|
28
|
-
from agentstack_cli.
|
|
28
|
+
from agentstack_cli.server_utils import announce_server_action, confirm_server_action
|
|
29
|
+
from agentstack_cli.utils import run_command, verbosity
|
|
29
30
|
|
|
30
31
|
app = AsyncTyper()
|
|
31
32
|
configuration = Configuration()
|
{agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/platform/__init__.py
RENAMED
|
@@ -17,7 +17,7 @@ import typer
|
|
|
17
17
|
from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_delay, wait_fixed
|
|
18
18
|
|
|
19
19
|
from agentstack_cli.async_typer import AsyncTyper
|
|
20
|
-
from agentstack_cli.commands.platform.base_driver import BaseDriver
|
|
20
|
+
from agentstack_cli.commands.platform.base_driver import BaseDriver, ImagePullMode
|
|
21
21
|
from agentstack_cli.commands.platform.lima_driver import LimaDriver
|
|
22
22
|
from agentstack_cli.commands.platform.wsl_driver import WSLDriver
|
|
23
23
|
from agentstack_cli.configuration import Configuration
|
|
@@ -64,19 +64,20 @@ async def start(
|
|
|
64
64
|
set_values_list: typing.Annotated[
|
|
65
65
|
list[str], typer.Option("--set", help="Set Helm chart values using <key>=<value> syntax", default_factory=list)
|
|
66
66
|
],
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
image_pull_mode: typing.Annotated[
|
|
68
|
+
ImagePullMode,
|
|
69
69
|
typer.Option(
|
|
70
|
-
"--
|
|
70
|
+
"--image-pull-mode",
|
|
71
|
+
help=textwrap.dedent(
|
|
72
|
+
"""\
|
|
73
|
+
guest = pull all images inside VM
|
|
74
|
+
host = pull unavailable images on host, then import all
|
|
75
|
+
hybrid = import available images from host, pull the rest in VM
|
|
76
|
+
skip = skip explicit pull step (Kubernetes will attempt to pull missing images)
|
|
77
|
+
"""
|
|
78
|
+
),
|
|
71
79
|
),
|
|
72
|
-
],
|
|
73
|
-
pull_on_host: typing.Annotated[
|
|
74
|
-
bool,
|
|
75
|
-
typer.Option(
|
|
76
|
-
"--pull-on-host",
|
|
77
|
-
help="Pull images on host Docker daemon and import them instead of pulling inside the VM. Acts as a pull cache layer.",
|
|
78
|
-
),
|
|
79
|
-
] = False,
|
|
80
|
+
] = ImagePullMode.guest,
|
|
80
81
|
values_file: typing.Annotated[
|
|
81
82
|
pathlib.Path | None, typer.Option("-f", help="Set Helm chart values using yaml values file")
|
|
82
83
|
] = None,
|
|
@@ -101,10 +102,7 @@ async def start(
|
|
|
101
102
|
await driver.deploy(
|
|
102
103
|
set_values_list=set_values_list,
|
|
103
104
|
values_file=values_file_path,
|
|
104
|
-
|
|
105
|
-
pull_on_host=pull_on_host,
|
|
106
|
-
skip_pull=skip_pull,
|
|
107
|
-
skip_restart_deployments=skip_restart_deployments,
|
|
105
|
+
image_pull_mode=image_pull_mode,
|
|
108
106
|
)
|
|
109
107
|
|
|
110
108
|
if not no_wait_for_platform:
|