agentstack-cli 0.5.1rc2__tar.gz → 0.5.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 (26) hide show
  1. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/PKG-INFO +1 -1
  2. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/pyproject.toml +1 -1
  3. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/__init__.py +19 -13
  4. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/async_typer.py +8 -0
  5. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/agent.py +34 -17
  6. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/build.py +2 -2
  7. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/model.py +8 -5
  8. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/platform/__init__.py +19 -12
  9. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/platform/base_driver.py +24 -10
  10. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/platform/lima_driver.py +4 -3
  11. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/platform/wsl_driver.py +5 -1
  12. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/user.py +2 -4
  13. agentstack_cli-0.5.2/src/agentstack_cli/data/helm-chart.tgz +0 -0
  14. agentstack_cli-0.5.1rc2/src/agentstack_cli/commands/mcp.py +0 -150
  15. agentstack_cli-0.5.1rc2/src/agentstack_cli/data/helm-chart.tgz +0 -0
  16. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/README.md +0 -0
  17. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/api.py +0 -0
  18. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/auth_manager.py +0 -0
  19. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/__init__.py +0 -0
  20. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/platform/istio.py +0 -0
  21. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/self.py +0 -0
  22. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/commands/server.py +0 -0
  23. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/configuration.py +0 -0
  24. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/console.py +0 -0
  25. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/data/.gitignore +0 -0
  26. {agentstack_cli-0.5.1rc2 → agentstack_cli-0.5.2}/src/agentstack_cli/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: agentstack-cli
3
- Version: 0.5.1rc2
3
+ Version: 0.5.2
4
4
  Summary: Agent Stack CLI
5
5
  Author: IBM Corp.
6
6
  Requires-Dist: anyio~=4.10.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentstack-cli"
3
- version = "0.5.1-rc2"
3
+ version = "0.5.2"
4
4
  description = "Agent Stack CLI"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "IBM Corp." }]
@@ -10,7 +10,6 @@ import typer
10
10
 
11
11
  import agentstack_cli.commands.agent
12
12
  import agentstack_cli.commands.build
13
- import agentstack_cli.commands.mcp
14
13
  import agentstack_cli.commands.model
15
14
  import agentstack_cli.commands.platform
16
15
  import agentstack_cli.commands.self
@@ -29,14 +28,14 @@ Usage: agentstack [OPTIONS] COMMAND [ARGS]...
29
28
  ╭─ Getting Started ──────────────────────────────────────────────────────────╮
30
29
  │ ui Launch the web interface │
31
30
  │ list View all available agents │
31
+ │ info Show agent details │
32
32
  │ run Run an agent interactively │
33
33
  ╰────────────────────────────────────────────────────────────────────────────╯
34
34
 
35
- ╭─ Agent Management ─────────────────────────────────────────────────────────╮
35
+ ╭─ Agent Management [Admin only] ────────────────────────────────────────────╮
36
36
  │ add Install an agent (Docker, GitHub) │
37
37
  │ remove Uninstall an agent │
38
38
  │ update Update an agent │
39
- │ info Show agent details │
40
39
  │ logs Stream agent execution logs │
41
40
  │ env Manage agent environment variables │
42
41
  │ build Build an agent remotely │
@@ -44,13 +43,13 @@ Usage: agentstack [OPTIONS] COMMAND [ARGS]...
44
43
  ╰────────────────────────────────────────────────────────────────────────────╯
45
44
 
46
45
  ╭─ Platform & Configuration ─────────────────────────────────────────────────╮
47
- │ model Configure 15+ LLM providers
48
- │ platform Start, stop, or delete local platform
46
+ │ model Configure 15+ LLM providers [Admin only]
47
+ │ platform Start, stop, or delete local platform [Local only]
49
48
  │ server Connect to remote Agent Stack servers │
50
- │ user Manage users and roles
49
+ │ user Manage users and roles [Admin only]
51
50
  │ self version Show Agent Stack CLI and Platform version │
52
- │ self upgrade Upgrade Agent Stack CLI and Platform
53
- │ self uninstall Uninstall Agent Stack CLI and Platform
51
+ │ self upgrade Upgrade Agent Stack CLI and Platform [Local only]
52
+ │ self uninstall Uninstall Agent Stack CLI and Platform [Local only]
54
53
  ╰────────────────────────────────────────────────────────────────────────────╯
55
54
 
56
55
  ╭─ Options ──────────────────────────────────────────────────────────────────╮
@@ -74,13 +73,20 @@ def main(
74
73
  raise typer.Exit()
75
74
 
76
75
 
77
- app.add_typer(agentstack_cli.commands.model.app, name="model", no_args_is_help=True, help="Manage model providers.")
78
- app.add_typer(agentstack_cli.commands.agent.app, name="agent", no_args_is_help=True, help="Manage agents.")
79
76
  app.add_typer(
80
- agentstack_cli.commands.platform.app, name="platform", no_args_is_help=True, help="Manage Agent Stack platform."
77
+ agentstack_cli.commands.model.app, name="model", no_args_is_help=True, help="Manage model providers. [Admin only]"
81
78
  )
82
79
  app.add_typer(
83
- agentstack_cli.commands.mcp.app, name="mcp", no_args_is_help=True, help="Manage MCP servers and toolkits."
80
+ agentstack_cli.commands.agent.app,
81
+ name="agent",
82
+ no_args_is_help=True,
83
+ help="Manage agents. Some commands are [Admin only].",
84
+ )
85
+ app.add_typer(
86
+ agentstack_cli.commands.platform.app,
87
+ name="platform",
88
+ no_args_is_help=True,
89
+ help="Manage Agent Stack platform. [Local only]",
84
90
  )
85
91
  app.add_typer(agentstack_cli.commands.build.app, name="", no_args_is_help=True, help="Build agent images.")
86
92
  app.add_typer(
@@ -100,7 +106,7 @@ app.add_typer(
100
106
  agentstack_cli.commands.user.app,
101
107
  name="user",
102
108
  no_args_is_help=True,
103
- help="Manage users.",
109
+ help="Manage users. [Admin only]",
104
110
  )
105
111
 
106
112
 
@@ -82,15 +82,23 @@ class AsyncTyper(typer.Typer):
82
82
  else:
83
83
  return f(*args, **kwargs)
84
84
  except* Exception as ex:
85
+ is_permission_error = False
85
86
  is_connect_error = False
86
87
  for exc_type, message in extract_messages(ex):
87
88
  err_console.print(format_error(exc_type, message))
88
89
  is_connect_error = is_connect_error or exc_type in ["ConnectionError", "ConnectError"]
90
+ is_permission_error = is_permission_error or (
91
+ exc_type == "HTTPStatusError" and "403" in message
92
+ )
89
93
  err_console.print()
90
94
  if is_connect_error:
91
95
  err_console.hint(
92
96
  "Start the Agent Stack platform using: [green]agentstack platform start[/green]. If that does not help, run [green]agentstack platform delete[/green] to clean up, then [green]agentstack platform start[/green] again."
93
97
  )
98
+ elif is_permission_error:
99
+ err_console.hint(
100
+ "This command requires higher permissions than your account currently has. Contact your administrator for assistance."
101
+ )
94
102
  else:
95
103
  err_console.hint(
96
104
  "Are you having consistent problems? If so, try these troubleshooting steps: [green]agentstack platform delete[/green] to remove the platform, and [green]agentstack platform start[/green] to recreate it."
@@ -151,16 +151,32 @@ class ProviderUtils(BaseModel):
151
151
  app = AsyncTyper()
152
152
 
153
153
  processing_messages = [
154
- "Buzzing with ideas...",
155
- "Pollinating thoughts...",
156
- "Honey of an answer coming up...",
157
- "Swarming through data...",
158
- "Bee-processing your request...",
159
- "Hive mind activating...",
160
- "Making cognitive honey...",
161
- "Waggle dancing for answers...",
162
- "Bee right back...",
163
- "Extracting knowledge nectar...",
154
+ "Asking agents...",
155
+ "Booting up bots...",
156
+ "Calibrating cognition...",
157
+ "Directing drones...",
158
+ "Engaging engines...",
159
+ "Fetching functions...",
160
+ "Gathering goals...",
161
+ "Hardening hypotheses...",
162
+ "Interpreting intentions...",
163
+ "Juggling judgements...",
164
+ "Kernelizing knowledge...",
165
+ "Loading logic...",
166
+ "Mobilizing models...",
167
+ "Nudging networks...",
168
+ "Optimizing outputs...",
169
+ "Prompting pipelines...",
170
+ "Quantizing queries...",
171
+ "Refining responses...",
172
+ "Scaling stacks...",
173
+ "Tuning transformers...",
174
+ "Unifying understandings...",
175
+ "Vectorizing values...",
176
+ "Wiring workflows...",
177
+ "Xecuting xperiments...",
178
+ "Yanking YAMLs...",
179
+ "Zipping zettabytes...",
164
180
  ]
165
181
 
166
182
  configuration = Configuration()
@@ -175,7 +191,7 @@ async def add_agent(
175
191
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
176
192
  yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
177
193
  ) -> None:
178
- """Add a docker image or GitHub repository.
194
+ """Add a docker image or GitHub repository. [Admin only]
179
195
 
180
196
  This command supports a variety of GitHub URL formats for deploying agents:
181
197
 
@@ -260,7 +276,7 @@ async def update_agent(
260
276
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
261
277
  yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
262
278
  ) -> None:
263
- """Upgrade agent to a newer docker image or build from GitHub repository"""
279
+ """Upgrade agent to a newer docker image or build from GitHub repository. [Admin only]"""
264
280
  with verbosity(verbose):
265
281
  async with configuration.use_platform_client():
266
282
  providers = await Provider.list()
@@ -389,7 +405,7 @@ async def uninstall_agent(
389
405
  yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
390
406
  all: typing.Annotated[bool, typer.Option("--all", "-a", help="Remove all agents without selection.")] = False,
391
407
  ) -> None:
392
- """Remove agent"""
408
+ """Remove agent. [Admin only]"""
393
409
  if search_path and all:
394
410
  console.error(
395
411
  "[bold]Cannot specify both --all and a search path."
@@ -440,7 +456,7 @@ async def stream_logs(
440
456
  str, typer.Argument(..., help="Short ID, agent name or part of the provider location")
441
457
  ],
442
458
  ):
443
- """Stream agent provider logs"""
459
+ """Stream agent provider logs. [Admin only]"""
444
460
  announce_server_action(f"Streaming logs for '{search_path}' from")
445
461
  async with configuration.use_platform_client():
446
462
  provider = select_provider(search_path, await Provider.list()).id
@@ -1270,7 +1286,7 @@ async def add_env(
1270
1286
  env: typing.Annotated[list[str], typer.Argument(help="Environment variables to pass to agent")],
1271
1287
  yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
1272
1288
  ) -> None:
1273
- """Store environment variables"""
1289
+ """Store environment variables. [Admin only]"""
1274
1290
  url = announce_server_action(f"Adding environment variables for '{search_path}' on")
1275
1291
  await confirm_server_action("Apply these environment variable changes on", url=url, yes=yes)
1276
1292
  env_vars = dict(parse_env_var(var) for var in env)
@@ -1286,7 +1302,7 @@ async def list_env(
1286
1302
  str, typer.Argument(..., help="Short ID, agent name or part of the provider location")
1287
1303
  ],
1288
1304
  ):
1289
- """List stored environment variables"""
1305
+ """List stored environment variables. [Admin only]"""
1290
1306
  announce_server_action(f"Listing environment variables for '{search_path}' on")
1291
1307
  async with configuration.use_platform_client():
1292
1308
  provider = select_provider(search_path, await Provider.list())
@@ -1301,6 +1317,7 @@ async def remove_env(
1301
1317
  env: typing.Annotated[list[str], typer.Argument(help="Environment variable(s) to remove")],
1302
1318
  yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
1303
1319
  ):
1320
+ """Remove environment variable(s). [Admin only]"""
1304
1321
  url = announce_server_action(f"Removing environment variables from '{search_path}' on")
1305
1322
  await confirm_server_action("Remove the selected environment variables on", url=url, yes=yes)
1306
1323
  async with configuration.use_platform_client():
@@ -1321,7 +1338,7 @@ async def list_feedback(
1321
1338
  limit: typing.Annotated[int, typer.Option("--limit", help="Number of results per page [default: 50]")] = 50,
1322
1339
  after_cursor: typing.Annotated[str | None, typer.Option("--after", help="Cursor for pagination")] = None,
1323
1340
  ):
1324
- """List your agent feedback"""
1341
+ """List your agent feedback. [Admin only]"""
1325
1342
 
1326
1343
  announce_server_action("Listing feedback on")
1327
1344
 
@@ -61,7 +61,7 @@ async def client_side_build(
61
61
  vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
62
62
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
63
63
  ):
64
- """Build agent locally using Docker."""
64
+ """Build agent locally using Docker. [Local only]"""
65
65
  with verbosity(verbose):
66
66
  await run_command(["which", "docker"], "Checking docker")
67
67
  image_id = "agentstack-agent-build-tmp:latest"
@@ -210,7 +210,7 @@ async def server_side_build(
210
210
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
211
211
  yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
212
212
  ):
213
- """Build agent from a GitHub repository in the platform."""
213
+ """Build agent from a GitHub repository in the platform. [Admin only]"""
214
214
 
215
215
  url = announce_server_action(f"Starting build for '{github_url}' on")
216
216
  await confirm_server_action("Proceed with building this agent on", url=url, yes=yes)
@@ -379,6 +379,7 @@ async def _select_default_model(capability: ModelCapability) -> str | None:
379
379
 
380
380
  @app.command("list")
381
381
  async def list_models():
382
+ """List all available models."""
382
383
  announce_server_action("Listing models on")
383
384
  async with configuration.use_platform_client():
384
385
  config = await SystemConfiguration.get()
@@ -411,7 +412,7 @@ async def _reset_configuration(existing_providers: list[ModelProvider] | None =
411
412
  await SystemConfiguration.update(default_embedding_model=None, default_llm_model=None)
412
413
 
413
414
 
414
- @app.command("setup")
415
+ @app.command("setup", help="Interactive setup for LLM and embedding provider environment variables [Admin only]")
415
416
  async def setup(
416
417
  use_true_localhost: typing.Annotated[bool, typer.Option(hidden=True)] = False,
417
418
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
@@ -477,7 +478,7 @@ async def setup(
477
478
  raise
478
479
 
479
480
 
480
- @app.command("change | select | default")
481
+ @app.command("change | select | default", help="Change the default model [Admin only]")
481
482
  async def select_default_model(
482
483
  capability: typing.Annotated[
483
484
  ModelCapability | None, typer.Argument(help="Which default model to change (llm/embedding)")
@@ -524,19 +525,21 @@ def _list_providers(providers: list[ModelProvider]):
524
525
 
525
526
  @model_provider_app.command("list")
526
527
  async def list_model_providers():
528
+ """List all available model providers."""
527
529
  announce_server_action("Listing model providers on")
528
530
  async with configuration.use_platform_client():
529
531
  providers = await ModelProvider.list()
530
532
  _list_providers(providers)
531
533
 
532
534
 
533
- @model_provider_app.command("add")
535
+ @model_provider_app.command("add", help="Add a new model provider [Admin only]")
534
536
  @app.command("add")
535
537
  async def add_provider(
536
538
  capability: typing.Annotated[
537
539
  ModelCapability | None, typer.Argument(help="Which default model to change (llm/embedding)")
538
540
  ] = None,
539
541
  ):
542
+ """Add a new model provider. [Admin only]"""
540
543
  announce_server_action("Adding provider for")
541
544
  if not capability:
542
545
  capability = await inquirer.select( # type: ignore
@@ -577,7 +580,7 @@ def _select_provider(providers: list[ModelProvider], search_path: str) -> ModelP
577
580
  return selected_provider
578
581
 
579
582
 
580
- @model_provider_app.command("remove | rm | delete")
583
+ @model_provider_app.command("remove | rm | delete", help="Remove a model provider [Admin only]")
581
584
  @app.command("remove | rm | delete")
582
585
  async def remove_provider(
583
586
  search_path: typing.Annotated[
@@ -639,7 +642,7 @@ async def ensure_llm_provider():
639
642
  if config.default_llm_model and not inconsistent:
640
643
  return
641
644
 
642
- console.print("[bold]Welcome to 🐝 [red]Agent Stack[/red]![/bold]")
645
+ console.print("[bold]Welcome to [red]Agent Stack[/red]![/bold]")
643
646
  console.print("Let's start by configuring your LLM environment.\n")
644
647
  try:
645
648
  await setup()
@@ -57,7 +57,7 @@ def get_driver(vm_name: str = "agentstack") -> BaseDriver:
57
57
  sys.exit(1)
58
58
 
59
59
 
60
- @app.command("start")
60
+ @app.command("start", help="Start Agent Stack platform. [Local only]")
61
61
  async def start(
62
62
  set_values_list: typing.Annotated[
63
63
  list[str], typer.Option("--set", help="Set Helm chart values using <key>=<value> syntax", default_factory=list)
@@ -68,13 +68,19 @@ async def start(
68
68
  "--import", help="Import an image from a local Docker CLI into Agent Stack platform", default_factory=list
69
69
  ),
70
70
  ],
71
+ pull_on_host: typing.Annotated[
72
+ bool,
73
+ typer.Option(
74
+ "--pull-on-host",
75
+ help="Pull images on host Docker daemon and import them instead of pulling inside the VM. Acts as a pull cache layer.",
76
+ ),
77
+ ] = False,
71
78
  values_file: typing.Annotated[
72
79
  pathlib.Path | None, typer.Option("-f", help="Set Helm chart values using yaml values file")
73
80
  ] = None,
74
81
  vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
75
82
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
76
83
  ):
77
- """Start Agent Stack platform."""
78
84
  import agentstack_cli.commands.server
79
85
 
80
86
  values_file_path = None
@@ -87,7 +93,12 @@ async def start(
87
93
  driver = get_driver(vm_name=vm_name)
88
94
  await driver.create_vm()
89
95
  await driver.install_tools()
90
- await driver.deploy(set_values_list=set_values_list, values_file=values_file_path, import_images=import_images)
96
+ await driver.deploy(
97
+ set_values_list=set_values_list,
98
+ values_file=values_file_path,
99
+ import_images=import_images,
100
+ pull_on_host=pull_on_host,
101
+ )
91
102
 
92
103
  with console.status("Waiting for Agent Stack platform to be ready...", spinner="dots"):
93
104
  timeout = datetime.timedelta(minutes=20)
@@ -124,12 +135,11 @@ async def start(
124
135
  await agentstack_cli.commands.server.server_login("http://localhost:8333")
125
136
 
126
137
 
127
- @app.command("stop")
138
+ @app.command("stop", help="Stop Agent Stack platform. [Local only]")
128
139
  async def stop(
129
140
  vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
130
141
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
131
142
  ):
132
- """Stop Agent Stack platform."""
133
143
  with verbosity(verbose):
134
144
  driver = get_driver(vm_name=vm_name)
135
145
  if not await driver.status():
@@ -139,40 +149,37 @@ async def stop(
139
149
  console.success("Agent Stack platform stopped successfully.")
140
150
 
141
151
 
142
- @app.command("delete")
152
+ @app.command("delete", help="Delete Agent Stack platform. [Local only]")
143
153
  async def delete(
144
154
  vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
145
155
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
146
156
  ):
147
- """Delete Agent Stack platform."""
148
157
  with verbosity(verbose):
149
158
  driver = get_driver(vm_name=vm_name)
150
159
  await driver.delete()
151
160
  console.success("Agent Stack platform deleted successfully.")
152
161
 
153
162
 
154
- @app.command("import")
163
+ @app.command("import", help="Import a local docker image into the Agent Stack platform. [Local only]")
155
164
  async def import_image_cmd(
156
165
  tag: typing.Annotated[str, typer.Argument(help="Docker image tag to import")],
157
166
  vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
158
167
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
159
168
  ):
160
- """Import a local docker image into the Agent Stack platform."""
161
169
  with verbosity(verbose):
162
170
  driver = get_driver(vm_name=vm_name)
163
171
  if (await driver.status()) != "running":
164
172
  console.error("Agent Stack platform is not running.")
165
173
  sys.exit(1)
166
- await driver.import_image(tag)
174
+ await driver.import_images(tag)
167
175
 
168
176
 
169
- @app.command("exec")
177
+ @app.command("exec", help="For debugging -- execute a command inside the Agent Stack platform VM. [Local only]")
170
178
  async def exec_cmd(
171
179
  command: typing.Annotated[list[str] | None, typer.Argument()] = None,
172
180
  vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
173
181
  verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
174
182
  ):
175
- """For debugging -- execute a command inside the Agent Stack platform VM."""
176
183
  with verbosity(verbose, show_success_status=False):
177
184
  driver = get_driver(vm_name=vm_name)
178
185
  if (await driver.status()) != "running":
@@ -15,6 +15,7 @@ from tenacity import AsyncRetrying, stop_after_attempt
15
15
 
16
16
  import agentstack_cli.commands.platform.istio
17
17
  from agentstack_cli.configuration import Configuration
18
+ from agentstack_cli.utils import run_command
18
19
 
19
20
 
20
21
  class BaseDriver(abc.ABC):
@@ -46,7 +47,7 @@ class BaseDriver(abc.ABC):
46
47
  async def delete(self) -> None: ...
47
48
 
48
49
  @abc.abstractmethod
49
- async def import_image(self, tag: str) -> None: ...
50
+ async def import_images(self, *tags: str) -> None: ...
50
51
 
51
52
  @abc.abstractmethod
52
53
  async def import_image_to_internal_registry(self, tag: str) -> None: ...
@@ -104,6 +105,7 @@ class BaseDriver(abc.ABC):
104
105
  set_values_list: list[str],
105
106
  values_file: pathlib.Path | None = None,
106
107
  import_images: list[str] | None = None,
108
+ pull_on_host: bool = False,
107
109
  ) -> None:
108
110
  await self.run_in_vm(
109
111
  ["sh", "-c", "mkdir -p /tmp/agentstack && cat >/tmp/agentstack/chart.tgz"],
@@ -140,22 +142,34 @@ class BaseDriver(abc.ABC):
140
142
  "Listing necessary images",
141
143
  )
142
144
  ).stdout.decode()
143
- for image in import_images or []:
144
- await self.import_image(image)
145
- self.loaded_images.add(image)
146
- for image in {typing.cast(str, yaml.safe_load(line)) for line in images_str.splitlines()} - set(
147
- import_images or []
148
- ):
145
+
146
+ def canonify(tag: str) -> str:
147
+ return tag if "." in tag.split("/")[0] else f"docker.io/{tag}"
148
+
149
+ required_images = {canonify(typing.cast(str, yaml.safe_load(line))) for line in images_str.splitlines()}
150
+ images_to_import = {canonify(tag) for tag in import_images or []}
151
+ images_to_pull = required_images - images_to_import
152
+
153
+ if pull_on_host:
154
+ for image in images_to_pull:
155
+ await run_command(["docker", "pull", image], f"Pulling image {image} on host")
156
+ images_to_import = required_images
157
+ images_to_pull = set[str]()
158
+
159
+ if images_to_import:
160
+ await self.import_images(*images_to_import)
161
+
162
+ for image in images_to_pull:
149
163
  async for attempt in AsyncRetrying(stop=stop_after_attempt(5)):
150
164
  with attempt:
151
165
  attempt_num = attempt.retry_state.attempt_number
152
- image_id = image if "." in image.split("/")[0] else f"docker.io/{image}"
153
- self.loaded_images.add(image_id)
154
166
  await self.run_in_vm(
155
- ["k3s", "ctr", "image", "pull", image_id],
167
+ ["k3s", "ctr", "image", "pull", image],
156
168
  f"Pulling image {image}" + (f" (attempt {attempt_num})" if attempt_num > 1 else ""),
157
169
  )
158
170
 
171
+ self.loaded_images = required_images
172
+
159
173
  if any("auth.oidc.enabled=true" in value.lower() for value in set_values_list):
160
174
  await agentstack_cli.commands.platform.istio.install(driver=self)
161
175
 
@@ -180,7 +180,7 @@ class LimaDriver(BaseDriver):
180
180
  )
181
181
 
182
182
  @typing.override
183
- async def import_image(self, tag: str):
183
+ async def import_images(self, *tags: str):
184
184
  image_dir = anyio.Path("/tmp/agentstack")
185
185
  await image_dir.mkdir(exist_ok=True, parents=True)
186
186
  image_file = str(uuid.uuid4())
@@ -188,11 +188,12 @@ class LimaDriver(BaseDriver):
188
188
 
189
189
  try:
190
190
  await run_command(
191
- ["docker", "image", "save", "-o", str(image_path), tag], f"Exporting image {tag} from Docker"
191
+ ["docker", "image", "save", "-o", str(image_path), *tags],
192
+ f"Exporting image{'' if len(tags) == 1 else 's'} {', '.join(tags)} from Docker",
192
193
  )
193
194
  await self.run_in_vm(
194
195
  ["/bin/sh", "-c", f"k3s ctr images import /tmp/agentstack/{image_file}"],
195
- f"Importing image {tag} into Agent Stack platform",
196
+ f"Importing image{'' if len(tags) == 1 else 's'} {', '.join(tags)} into Agent Stack platform",
196
197
  )
197
198
  finally:
198
199
  await image_path.unlink(missing_ok=True)
@@ -131,7 +131,11 @@ class WSLDriver(BaseDriver):
131
131
  set_values_list: list[str],
132
132
  values_file: pathlib.Path | None = None,
133
133
  import_images: list[str] | None = None,
134
+ pull_on_host: bool = False,
134
135
  ) -> None:
136
+ if pull_on_host:
137
+ raise NotImplementedError("Pulling on host is not supported on this platform.")
138
+
135
139
  host_ip = (
136
140
  (
137
141
  await self.run_in_vm(
@@ -205,7 +209,7 @@ class WSLDriver(BaseDriver):
205
209
  await run_command(["wsl.exe", "--unregister", self.vm_name], "Deleting Agent Stack platform", check=False)
206
210
 
207
211
  @typing.override
208
- async def import_image(self, tag: str) -> None:
212
+ async def import_images(self, *tags: str) -> None:
209
213
  raise NotImplementedError("Importing images is not supported on this platform.")
210
214
 
211
215
  @typing.override
@@ -23,13 +23,12 @@ ROLE_DISPLAY = {
23
23
  }
24
24
 
25
25
 
26
- @app.command("list")
26
+ @app.command("list", help="List platform users [Admin only]")
27
27
  async def list_users(
28
28
  email: typing.Annotated[str | None, typer.Option(help="Filter by email (case-insensitive partial match)")] = None,
29
29
  limit: typing.Annotated[int, typer.Option(help="Results per page (1-100)")] = 40,
30
30
  after: typing.Annotated[str | None, typer.Option(help="Pagination cursor (page_token)")] = None,
31
31
  ):
32
- """List platform users (admin only)."""
33
32
  announce_server_action("Listing users on")
34
33
 
35
34
  async with configuration.use_platform_client():
@@ -68,13 +67,12 @@ async def list_users(
68
67
  console.print(f"\n[dim]Use --after {next_page_token} to see more[/dim]")
69
68
 
70
69
 
71
- @app.command("set-role")
70
+ @app.command("set-role", help="Change user role [Admin only]")
72
71
  async def set_role(
73
72
  user_id: typing.Annotated[str, typer.Argument(help="User UUID")],
74
73
  role: typing.Annotated[UserRole, typer.Argument(help="Target role (admin, developer, user)")],
75
74
  yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
76
75
  ):
77
- """Change user role (admin only)."""
78
76
  url = announce_server_action(f"Changing user {user_id} to role '{role}' on")
79
77
  await confirm_server_action("Proceed with role change on", url=url, yes=yes)
80
78
 
@@ -1,150 +0,0 @@
1
- # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- import typing
5
- from enum import StrEnum
6
-
7
- import typer
8
- from rich.table import Column
9
-
10
- from agentstack_cli.api import api_request
11
- from agentstack_cli.async_typer import AsyncTyper, console, create_table
12
- from agentstack_cli.utils import announce_server_action, confirm_server_action, status
13
-
14
- app = AsyncTyper()
15
-
16
-
17
- class Transport(StrEnum):
18
- SSE = "sse"
19
- STREAMABLE_HTTP = "streamable_http"
20
-
21
-
22
- @app.command("add")
23
- async def add_provider(
24
- name: typing.Annotated[str, typer.Argument(help="Name for the MCP server")],
25
- location: typing.Annotated[str, typer.Argument(help="Location of the MCP server")],
26
- transport: typing.Annotated[
27
- Transport, typer.Argument(help="Transport the MCP server uses")
28
- ] = Transport.STREAMABLE_HTTP,
29
- yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
30
- ) -> None:
31
- """Install discovered MCP server."""
32
-
33
- url = announce_server_action(f"Registering MCP server '{name}' on")
34
- await confirm_server_action("Proceed with registering this MCP server on", url=url, yes=yes)
35
- with status("Registering server to platform"):
36
- await api_request(
37
- "POST", "mcp/providers", json={"name": name, "location": location, "transport": transport.value}
38
- )
39
- console.print("Registering server to platform [[green]DONE[/green]]")
40
- await list_providers()
41
-
42
-
43
- @app.command("list")
44
- async def list_providers():
45
- """List MCP servers."""
46
-
47
- announce_server_action("Listing MCP servers on")
48
- providers = await api_request("GET", "mcp/providers")
49
- assert providers
50
- with create_table(
51
- Column("Name"),
52
- Column("Location"),
53
- Column("Transport"),
54
- Column("State"),
55
- no_wrap=True,
56
- ) as table:
57
- for provider in providers:
58
- table.add_row(provider["name"], provider["location"], provider["transport"], provider["state"])
59
- console.print()
60
- console.print(table)
61
-
62
-
63
- @app.command("remove | uninstall | rm | delete")
64
- async def uninstall_provider(
65
- name: typing.Annotated[str, typer.Argument(help="Name of the MCP provider to remove")],
66
- yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
67
- ) -> None:
68
- """Remove MCP server."""
69
- url = announce_server_action(f"Removing MCP server '{name}' from")
70
- await confirm_server_action("Proceed with removing this MCP server from", url=url, yes=yes)
71
- provider = await _get_provider_by_name(name)
72
- if provider:
73
- await api_request("delete", f"mcp/providers/{provider['id']}")
74
- else:
75
- raise ValueError(f"Provider {name} not found")
76
- await list_providers()
77
-
78
-
79
- tool_app = AsyncTyper()
80
- app.add_typer(tool_app, name="tool", no_args_is_help=True, help="Inspect tools.")
81
-
82
-
83
- @tool_app.command("list")
84
- async def list_tools() -> None:
85
- """List tools."""
86
-
87
- announce_server_action("Listing MCP tools on")
88
- tools = await api_request("GET", "mcp/tools")
89
- assert tools
90
- with create_table(
91
- Column("Name"),
92
- Column("Description", max_width=30),
93
- no_wrap=True,
94
- ) as table:
95
- for tool in tools:
96
- table.add_row(tool["name"], tool["description"])
97
- console.print()
98
- console.print(table)
99
-
100
-
101
- toolkit_app = AsyncTyper()
102
- app.add_typer(toolkit_app, name="toolkit", no_args_is_help=True, help="Create toolkits.")
103
-
104
-
105
- @toolkit_app.command("create")
106
- async def toolkit(
107
- tools: typing.Annotated[list[str], typer.Argument(help="Tools to put in the toolkit")],
108
- yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
109
- ) -> None:
110
- """Create a toolkit."""
111
-
112
- url = announce_server_action("Creating MCP toolkit on")
113
- await confirm_server_action("Proceed with creating this MCP toolkit on", url=url, yes=yes)
114
- api_tools = await _get_tools_by_names(tools)
115
- assert api_tools
116
- toolkit = await api_request("POST", "mcp/toolkits", json={"tools": [tool["id"] for tool in api_tools]})
117
- assert toolkit
118
- with create_table(Column("Location"), Column("Transport"), Column("Expiration")) as table:
119
- table.add_row(toolkit["location"], toolkit["transport"], toolkit["expires_at"])
120
- console.print()
121
- console.print(table)
122
-
123
-
124
- async def _get_provider_by_name(name: str):
125
- providers = await api_request("GET", "mcp/providers")
126
- assert providers
127
-
128
- for provider in providers:
129
- if provider["name"] == name:
130
- return provider
131
-
132
- raise ValueError(f"Provider {name} not found")
133
-
134
-
135
- async def _get_tools_by_names(names: list[str]) -> list[dict[str, typing.Any]]:
136
- all_tools = await api_request("GET", "mcp/tools")
137
- assert all_tools
138
-
139
- tools = []
140
- for name in names:
141
- found = False
142
- for tool in all_tools:
143
- if tool["name"] == name:
144
- tools.append(tool)
145
- found = True
146
- break
147
- if not found:
148
- raise ValueError(f"Tool {name} not found")
149
-
150
- return tools