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.
Files changed (29) hide show
  1. agentstack_cli-0.6.1rc1/PKG-INFO +26 -0
  2. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/pyproject.toml +21 -20
  3. agentstack_cli-0.6.1rc1/src/agentstack_cli/auth_manager.py +294 -0
  4. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/agent.py +43 -5
  5. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/build.py +1 -2
  6. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/connector.py +5 -4
  7. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/model.py +2 -1
  8. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/platform/__init__.py +14 -16
  9. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/platform/base_driver.py +101 -39
  10. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/platform/lima_driver.py +2 -0
  11. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/platform/wsl_driver.py +5 -8
  12. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/self.py +2 -2
  13. agentstack_cli-0.6.1rc1/src/agentstack_cli/commands/server.py +366 -0
  14. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/user.py +1 -1
  15. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/configuration.py +21 -1
  16. agentstack_cli-0.6.1rc1/src/agentstack_cli/data/helm-chart.tgz +0 -0
  17. agentstack_cli-0.6.1rc1/src/agentstack_cli/server_utils.py +40 -0
  18. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/utils.py +18 -34
  19. agentstack_cli-0.6.0rc3/PKG-INFO +0 -26
  20. agentstack_cli-0.6.0rc3/src/agentstack_cli/auth_manager.py +0 -242
  21. agentstack_cli-0.6.0rc3/src/agentstack_cli/commands/server.py +0 -315
  22. agentstack_cli-0.6.0rc3/src/agentstack_cli/data/helm-chart.tgz +0 -0
  23. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/README.md +0 -0
  24. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/__init__.py +0 -0
  25. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/api.py +0 -0
  26. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/async_typer.py +0 -0
  27. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/commands/__init__.py +0 -0
  28. {agentstack_cli-0.6.0rc3 → agentstack_cli-0.6.1rc1}/src/agentstack_cli/console.py +0 -0
  29. {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.0-rc3"
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~=4.10.0",
10
- "pydantic~=2.11.7",
11
- "pydantic-settings~=2.10.1",
12
- "requests~=2.32.5",
13
- "jsonschema~=4.25.1",
14
- "jsf~=0.11.2",
15
- 'gnureadline~=8.2.13; sys_platform != "win32"',
16
- "prompt-toolkit~=3.0.52",
17
- "inquirerpy~=0.3.4",
18
- "psutil~=7.0.0",
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~=9.1.2",
21
- "typer~=0.17.4",
22
- "pyyaml~=6.0.2",
20
+ "tenacity>=9.1.2",
21
+ "typer>=0.21.1",
22
+ "pyyaml>=6.0.3",
23
23
  "agentstack-sdk",
24
- "authlib~=1.6.3",
25
- "openai~=1.107.1",
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.399",
31
- "pytest>=8.3.4",
32
- "ruff>=0.8.5",
33
- "wheel>=0.45.1",
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
- with status("Registering agent to platform"):
261
- async with configuration.use_platform_client():
262
- await Provider.create(location=location)
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,
@@ -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.utils import (
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
- raise typer.Exit(1)
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
- raise typer.Exit(code=1) from None
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
- raise typer.Exit(1)
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.utils import announce_server_action, confirm_server_action, run_command, verbosity
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()
@@ -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
- import_images: typing.Annotated[
68
- list[str],
67
+ image_pull_mode: typing.Annotated[
68
+ ImagePullMode,
69
69
  typer.Option(
70
- "--import", help="Import an image from a local Docker CLI into Agent Stack platform", default_factory=list
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
- import_images=import_images,
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: