agentstack-cli 0.6.0rc1__py3-none-manylinux_2_34_aarch64.whl
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/__init__.py +164 -0
- agentstack_cli/api.py +160 -0
- agentstack_cli/async_typer.py +113 -0
- agentstack_cli/auth_manager.py +242 -0
- agentstack_cli/commands/__init__.py +3 -0
- agentstack_cli/commands/agent.py +1386 -0
- agentstack_cli/commands/build.py +222 -0
- agentstack_cli/commands/connector.py +301 -0
- agentstack_cli/commands/model.py +653 -0
- agentstack_cli/commands/platform/__init__.py +198 -0
- agentstack_cli/commands/platform/base_driver.py +217 -0
- agentstack_cli/commands/platform/lima_driver.py +277 -0
- agentstack_cli/commands/platform/wsl_driver.py +229 -0
- agentstack_cli/commands/self.py +213 -0
- agentstack_cli/commands/server.py +315 -0
- agentstack_cli/commands/user.py +87 -0
- agentstack_cli/configuration.py +79 -0
- agentstack_cli/console.py +25 -0
- agentstack_cli/data/.gitignore +2 -0
- agentstack_cli/data/helm-chart.tgz +0 -0
- agentstack_cli/data/lima-guestagent.Linux-aarch64.gz +0 -0
- agentstack_cli/data/limactl +0 -0
- agentstack_cli/utils.py +389 -0
- agentstack_cli-0.6.0rc1.dist-info/METADATA +107 -0
- agentstack_cli-0.6.0rc1.dist-info/RECORD +27 -0
- agentstack_cli-0.6.0rc1.dist-info/WHEEL +4 -0
- agentstack_cli-0.6.0rc1.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import functools
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
import packaging.version
|
|
13
|
+
import pydantic
|
|
14
|
+
import typer
|
|
15
|
+
from InquirerPy import inquirer
|
|
16
|
+
|
|
17
|
+
import agentstack_cli.commands.platform
|
|
18
|
+
from agentstack_cli.api import fetch_server_version
|
|
19
|
+
from agentstack_cli.async_typer import AsyncTyper
|
|
20
|
+
from agentstack_cli.commands.model import setup as model_setup
|
|
21
|
+
from agentstack_cli.configuration import Configuration
|
|
22
|
+
from agentstack_cli.console import console
|
|
23
|
+
from agentstack_cli.utils import run_command, verbosity
|
|
24
|
+
|
|
25
|
+
app = AsyncTyper()
|
|
26
|
+
configuration = Configuration()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@functools.cache
|
|
30
|
+
def _path() -> str:
|
|
31
|
+
# These are PATHs where `uv` installs itself when installed through own install script
|
|
32
|
+
# Package managers may install elsewhere, but that location should already be in PATH
|
|
33
|
+
return os.pathsep.join(
|
|
34
|
+
[
|
|
35
|
+
*([xdg_bin_home] if (xdg_bin_home := os.getenv("XDG_BIN_HOME")) else []),
|
|
36
|
+
*([os.path.realpath(f"{xdg_data_home}/../bin")] if (xdg_data_home := os.getenv("XDG_DATA_HOME")) else []),
|
|
37
|
+
os.path.expanduser("~/.local/bin"),
|
|
38
|
+
os.getenv("PATH", ""),
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command("version")
|
|
44
|
+
async def version(
|
|
45
|
+
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
46
|
+
):
|
|
47
|
+
"""Print version of the Agent Stack CLI."""
|
|
48
|
+
with verbosity(verbose=verbose):
|
|
49
|
+
cli_version = importlib.metadata.version("agentstack-cli")
|
|
50
|
+
platform_version = await fetch_server_version()
|
|
51
|
+
active_server = configuration.auth_manager.active_server
|
|
52
|
+
|
|
53
|
+
latest_cli_version: str | None = None
|
|
54
|
+
with console.status("Checking for newer version...", spinner="dots"):
|
|
55
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
56
|
+
response = await client.get("https://pypi.org/pypi/agentstack-cli/json")
|
|
57
|
+
PyPIPackageInfo = typing.TypedDict("PyPIPackageInfo", {"version": str})
|
|
58
|
+
PyPIPackage = typing.TypedDict("PyPIPackage", {"info": PyPIPackageInfo})
|
|
59
|
+
if response.status_code == 200:
|
|
60
|
+
latest_cli_version = pydantic.TypeAdapter(PyPIPackage).validate_json(response.text)["info"][
|
|
61
|
+
"version"
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
console.print()
|
|
65
|
+
console.print(f" agentstack-cli version: [bold]{cli_version}[/bold]")
|
|
66
|
+
console.print(
|
|
67
|
+
f"agentstack-platform version: [bold]{platform_version.replace('-', '') if platform_version is not None else 'not running'}[/bold]"
|
|
68
|
+
)
|
|
69
|
+
console.print(f" agentstack server: [bold]{active_server if active_server else 'none'}[/bold]")
|
|
70
|
+
console.print()
|
|
71
|
+
|
|
72
|
+
if latest_cli_version and packaging.version.parse(latest_cli_version) > packaging.version.parse(cli_version):
|
|
73
|
+
console.hint(
|
|
74
|
+
f"A newer version ([bold]{latest_cli_version}[/bold]) is available. Update using: [green]agentstack self upgrade[/green]."
|
|
75
|
+
)
|
|
76
|
+
elif platform_version is None:
|
|
77
|
+
console.hint("Start the Agent Stack platform using: [green]agentstack platform start[/green]")
|
|
78
|
+
elif platform_version.replace("-", "") != cli_version:
|
|
79
|
+
console.hint("Update the Agent Stack platform using: [green]agentstack platform start[/green]")
|
|
80
|
+
else:
|
|
81
|
+
console.success("Everything is up to date!")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("install")
|
|
85
|
+
async def install(
|
|
86
|
+
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
87
|
+
):
|
|
88
|
+
"""Install Agent Stack platform pre-requisites."""
|
|
89
|
+
with verbosity(verbose=verbose):
|
|
90
|
+
ready_to_start = False
|
|
91
|
+
if platform.system() == "Linux":
|
|
92
|
+
if shutil.which(
|
|
93
|
+
f"qemu-system-{'aarch64' if platform.machine().lower() == 'arm64' else platform.machine().lower()}"
|
|
94
|
+
):
|
|
95
|
+
ready_to_start = True
|
|
96
|
+
else:
|
|
97
|
+
if os.geteuid() != 0:
|
|
98
|
+
console.hint(
|
|
99
|
+
"You may be prompted for your password to install QEMU, as this needs root privileges."
|
|
100
|
+
)
|
|
101
|
+
os.execlp("sudo", sys.executable, *sys.argv)
|
|
102
|
+
for cmd in [
|
|
103
|
+
["apt", "install", "-y", "-qq", "qemu-system"],
|
|
104
|
+
["dnf", "install", "-y", "-q", "@virtualization"],
|
|
105
|
+
["pacman", "-S", "--noconfirm", "--noprogressbar", "qemu"],
|
|
106
|
+
["zypper", "install", "-y", "-qq", "qemu"],
|
|
107
|
+
["yum", "install", "-y", "-q", "qemu-kvm"],
|
|
108
|
+
["emerge", "--quiet", "app-emulation/qemu"],
|
|
109
|
+
]:
|
|
110
|
+
if shutil.which(cmd[0]):
|
|
111
|
+
try:
|
|
112
|
+
await run_command(cmd, f"Installing QEMU with {cmd[0]}")
|
|
113
|
+
ready_to_start = True
|
|
114
|
+
break
|
|
115
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
116
|
+
console.warning(
|
|
117
|
+
"Failed to install QEMU automatically. Please install QEMU manually before using Agent Stack. Refer to https://www.qemu.org/download/ for instructions."
|
|
118
|
+
)
|
|
119
|
+
break
|
|
120
|
+
elif platform.system() == "Darwin":
|
|
121
|
+
ready_to_start = True
|
|
122
|
+
|
|
123
|
+
already_started = False
|
|
124
|
+
console.print()
|
|
125
|
+
if (
|
|
126
|
+
ready_to_start
|
|
127
|
+
and await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
|
|
128
|
+
message="Do you want to start the Agent Stack platform now? Will run: agentstack platform start",
|
|
129
|
+
default=True,
|
|
130
|
+
).execute_async()
|
|
131
|
+
):
|
|
132
|
+
try:
|
|
133
|
+
await agentstack_cli.commands.platform.start(set_values_list=[], import_images=[], verbose=verbose)
|
|
134
|
+
already_started = True
|
|
135
|
+
console.print()
|
|
136
|
+
except Exception:
|
|
137
|
+
console.warning("Platform start failed. You can retry with [green]agentstack platform start[/green].")
|
|
138
|
+
|
|
139
|
+
already_configured = False
|
|
140
|
+
if (
|
|
141
|
+
already_started
|
|
142
|
+
and await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
|
|
143
|
+
message="Do you want to configure your LLM provider now? Will run: agentstack model setup", default=True
|
|
144
|
+
).execute_async()
|
|
145
|
+
):
|
|
146
|
+
try:
|
|
147
|
+
await model_setup(verbose=verbose)
|
|
148
|
+
already_configured = True
|
|
149
|
+
except Exception:
|
|
150
|
+
console.warning("Model setup failed. You can retry with [green]agentstack model setup[/green].")
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
already_configured
|
|
154
|
+
and await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
|
|
155
|
+
message="Do you want to open the web UI now? Will run: agentstack ui", default=True
|
|
156
|
+
).execute_async()
|
|
157
|
+
):
|
|
158
|
+
import webbrowser
|
|
159
|
+
|
|
160
|
+
webbrowser.open("http://localhost:8334")
|
|
161
|
+
|
|
162
|
+
console.print()
|
|
163
|
+
console.success("Installation complete!")
|
|
164
|
+
if not shutil.which("agentstack", path=_path()):
|
|
165
|
+
console.hint("Open a new terminal window to use the [green]agentstack[/green] command.")
|
|
166
|
+
if not already_started:
|
|
167
|
+
console.hint("Start the Agent Stack platform using: [green]agentstack platform start[/green]")
|
|
168
|
+
if not already_configured:
|
|
169
|
+
console.hint("Configure your LLM provider using: [green]agentstack model setup[/green]")
|
|
170
|
+
console.hint(
|
|
171
|
+
"Use [green]agentstack ui[/green] to open the web GUI, or [green]agentstack run chat[/green] to talk to an agent on the command line."
|
|
172
|
+
)
|
|
173
|
+
console.hint(
|
|
174
|
+
"Run [green]agentstack --help[/green] to learn about available commands, or check the documentation at https://agentstack.beeai.dev/"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command("upgrade")
|
|
179
|
+
async def upgrade(
|
|
180
|
+
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
181
|
+
):
|
|
182
|
+
"""Upgrade Agent Stack CLI and Platform to the latest version."""
|
|
183
|
+
if not shutil.which("uv", path=_path()):
|
|
184
|
+
console.error("Can't self-upgrade because 'uv' was not found.")
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
|
|
187
|
+
with verbosity(verbose=verbose):
|
|
188
|
+
await run_command(
|
|
189
|
+
["uv", "tool", "install", "--force", "agentstack-cli"],
|
|
190
|
+
"Upgrading agentstack-cli",
|
|
191
|
+
env={"PATH": _path()},
|
|
192
|
+
)
|
|
193
|
+
await agentstack_cli.commands.platform.start(set_values_list=[], import_images=[], verbose=verbose)
|
|
194
|
+
await version(verbose=verbose)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@app.command("uninstall")
|
|
198
|
+
async def uninstall(
|
|
199
|
+
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
200
|
+
):
|
|
201
|
+
"""Uninstall Agent Stack CLI and Platform."""
|
|
202
|
+
if not shutil.which("uv", path=_path()):
|
|
203
|
+
console.error("Can't self-uninstall because 'uv' was not found.")
|
|
204
|
+
raise typer.Exit(1)
|
|
205
|
+
|
|
206
|
+
with verbosity(verbose=verbose):
|
|
207
|
+
await agentstack_cli.commands.platform.delete(verbose=verbose)
|
|
208
|
+
await run_command(
|
|
209
|
+
["uv", "tool", "uninstall", "agentstack-cli"],
|
|
210
|
+
"Uninstalling agentstack-cli",
|
|
211
|
+
env={"PATH": _path()},
|
|
212
|
+
)
|
|
213
|
+
console.success("Agent Stack uninstalled successfully.")
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
import typing
|
|
8
|
+
import uuid
|
|
9
|
+
import webbrowser
|
|
10
|
+
from urllib.parse import urlencode
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import typer
|
|
14
|
+
import uvicorn
|
|
15
|
+
from authlib.common.security import generate_token
|
|
16
|
+
from authlib.oauth2.rfc7636 import create_s256_code_challenge
|
|
17
|
+
from fastapi import FastAPI, Request
|
|
18
|
+
from fastapi.responses import HTMLResponse
|
|
19
|
+
from InquirerPy import inquirer
|
|
20
|
+
from InquirerPy.base.control import Choice
|
|
21
|
+
|
|
22
|
+
from agentstack_cli.async_typer import AsyncTyper, console
|
|
23
|
+
from agentstack_cli.configuration import Configuration
|
|
24
|
+
|
|
25
|
+
app = AsyncTyper()
|
|
26
|
+
|
|
27
|
+
config = Configuration()
|
|
28
|
+
|
|
29
|
+
REDIRECT_URI = "http://localhost:9001/callback"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _wait_for_auth_code(port: int = 9001) -> str:
|
|
33
|
+
code_future: asyncio.Future[str] = asyncio.Future()
|
|
34
|
+
app = FastAPI()
|
|
35
|
+
|
|
36
|
+
@app.get("/callback")
|
|
37
|
+
async def callback(request: Request):
|
|
38
|
+
code = request.query_params.get("code")
|
|
39
|
+
if code and not code_future.done():
|
|
40
|
+
code_future.set_result(code)
|
|
41
|
+
return HTMLResponse(
|
|
42
|
+
content="""
|
|
43
|
+
<!DOCTYPE html>
|
|
44
|
+
<html>
|
|
45
|
+
<head>
|
|
46
|
+
<title>Login Successful</title>
|
|
47
|
+
<style>
|
|
48
|
+
body { font-family: Arial, sans-serif; text-align: center; margin-top: 15%; }
|
|
49
|
+
h1 { color: #2e7d32; }
|
|
50
|
+
p { color: #555; }
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<h1>Login successful!</h1>
|
|
55
|
+
<p>You can safely close this tab and return to the Agent Stack CLI.</p>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
58
|
+
""",
|
|
59
|
+
status_code=200,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
server = uvicorn.Server(config=uvicorn.Config(app, host="127.0.0.1", port=port, log_level=logging.ERROR))
|
|
63
|
+
|
|
64
|
+
async with asyncio.TaskGroup() as tg:
|
|
65
|
+
tg.create_task(server.serve())
|
|
66
|
+
code = await code_future
|
|
67
|
+
server.should_exit = True
|
|
68
|
+
|
|
69
|
+
return code
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_unique_app_name() -> str:
|
|
73
|
+
return f"Agent Stack CLI {uuid.uuid4()}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("login | change | select | default | switch")
|
|
77
|
+
async def server_login(server: typing.Annotated[str | None, typer.Argument()] = None):
|
|
78
|
+
"""Login to a server or switch between logged in servers."""
|
|
79
|
+
server = server or (
|
|
80
|
+
await inquirer.select( # type: ignore
|
|
81
|
+
message="Select a server, or log in to a new one:",
|
|
82
|
+
choices=[
|
|
83
|
+
*(
|
|
84
|
+
Choice(
|
|
85
|
+
name=f"{server} {'(active)' if server == config.auth_manager.active_server else ''}",
|
|
86
|
+
value=server,
|
|
87
|
+
)
|
|
88
|
+
for server in config.auth_manager.servers
|
|
89
|
+
),
|
|
90
|
+
Choice(name="Log in to a new server", value=None),
|
|
91
|
+
],
|
|
92
|
+
default=0,
|
|
93
|
+
).execute_async()
|
|
94
|
+
if config.auth_manager.servers
|
|
95
|
+
else None
|
|
96
|
+
)
|
|
97
|
+
server = server or await inquirer.text(message="Enter server URL:").execute_async() # type: ignore
|
|
98
|
+
|
|
99
|
+
if not server:
|
|
100
|
+
raise RuntimeError("No server selected. Action cancelled.")
|
|
101
|
+
|
|
102
|
+
if "://" not in server:
|
|
103
|
+
server = f"https://{server}"
|
|
104
|
+
|
|
105
|
+
server = server.rstrip("/")
|
|
106
|
+
|
|
107
|
+
if server_data := config.auth_manager.get_server(server):
|
|
108
|
+
console.info("Switching to an already logged in server.")
|
|
109
|
+
auth_server = None
|
|
110
|
+
auth_servers = list(server_data.authorization_servers.keys())
|
|
111
|
+
if len(auth_servers) == 1:
|
|
112
|
+
auth_server = auth_servers[0]
|
|
113
|
+
elif len(auth_servers) > 1:
|
|
114
|
+
auth_server = await inquirer.select( # type: ignore
|
|
115
|
+
message="Select an authorization server:",
|
|
116
|
+
choices=[
|
|
117
|
+
Choice(
|
|
118
|
+
name=f"{auth_server} {'(active)' if auth_server == config.auth_manager.active_auth_server else ''}",
|
|
119
|
+
value=auth_server,
|
|
120
|
+
)
|
|
121
|
+
for auth_server in auth_servers
|
|
122
|
+
],
|
|
123
|
+
default=config.auth_manager.active_auth_server
|
|
124
|
+
if config.auth_manager.active_auth_server in auth_servers
|
|
125
|
+
else 0,
|
|
126
|
+
).execute_async()
|
|
127
|
+
if not auth_server:
|
|
128
|
+
console.info("Action cancelled.")
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
else:
|
|
131
|
+
console.info("No authentication tokens found for this server. Proceeding to log in.")
|
|
132
|
+
async with httpx.AsyncClient() as client:
|
|
133
|
+
resp = await client.get(f"{server}/.well-known/oauth-protected-resource/", follow_redirects=True)
|
|
134
|
+
if resp.is_error:
|
|
135
|
+
console.error("This server does not appear to run a compatible version of Agent Stack Platform.")
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
metadata = resp.json()
|
|
138
|
+
|
|
139
|
+
auth_servers = metadata.get("authorization_servers", [])
|
|
140
|
+
auth_server = None
|
|
141
|
+
token = None
|
|
142
|
+
|
|
143
|
+
client_id = config.client_id
|
|
144
|
+
client_secret = config.client_secret
|
|
145
|
+
registration_token = None
|
|
146
|
+
|
|
147
|
+
if auth_servers:
|
|
148
|
+
if len(auth_servers) == 1:
|
|
149
|
+
auth_server = auth_servers[0]
|
|
150
|
+
else:
|
|
151
|
+
auth_server = await inquirer.select( # type: ignore
|
|
152
|
+
message="Select an authorization server:",
|
|
153
|
+
choices=auth_servers,
|
|
154
|
+
).execute_async()
|
|
155
|
+
|
|
156
|
+
if not auth_server:
|
|
157
|
+
raise RuntimeError("No authorization server selected.")
|
|
158
|
+
|
|
159
|
+
async with httpx.AsyncClient() as client:
|
|
160
|
+
try:
|
|
161
|
+
resp = await client.get(f"{auth_server}/.well-known/openid-configuration")
|
|
162
|
+
resp.raise_for_status()
|
|
163
|
+
oidc = resp.json()
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise RuntimeError(f"OIDC discovery failed: {e}") from e
|
|
166
|
+
|
|
167
|
+
registration_endpoint = oidc["registration_endpoint"]
|
|
168
|
+
if not client_id and registration_endpoint:
|
|
169
|
+
async with httpx.AsyncClient() as client:
|
|
170
|
+
resp = None
|
|
171
|
+
try:
|
|
172
|
+
app_name = get_unique_app_name()
|
|
173
|
+
resp = await client.post(
|
|
174
|
+
registration_endpoint,
|
|
175
|
+
json={
|
|
176
|
+
"client_name": app_name,
|
|
177
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
178
|
+
"enforce_pkce": True,
|
|
179
|
+
"all_users_entitled": True,
|
|
180
|
+
"redirect_uris": [REDIRECT_URI],
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
resp.raise_for_status()
|
|
184
|
+
data = resp.json()
|
|
185
|
+
client_id = data["client_id"]
|
|
186
|
+
client_secret = data["client_secret"]
|
|
187
|
+
registration_token = data["registration_access_token"]
|
|
188
|
+
except Exception as e:
|
|
189
|
+
if resp:
|
|
190
|
+
try:
|
|
191
|
+
error_details = resp.json()
|
|
192
|
+
console.warning(
|
|
193
|
+
f"error: {error_details['error']} error description: {error_details['error_description']}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
except Exception:
|
|
197
|
+
console.info("no parsable json response.")
|
|
198
|
+
console.warning(f" Dynamic client registration failed. Proceed with manual input. {e!s}")
|
|
199
|
+
|
|
200
|
+
if not client_id:
|
|
201
|
+
client_id = (
|
|
202
|
+
await inquirer.text( # type: ignore
|
|
203
|
+
message="Enter Client ID (default agentstack-cli):",
|
|
204
|
+
instruction=f"(Redirect URI: {REDIRECT_URI})",
|
|
205
|
+
).execute_async()
|
|
206
|
+
or "agentstack-cli"
|
|
207
|
+
)
|
|
208
|
+
if not client_id:
|
|
209
|
+
raise RuntimeError("Client ID is mandatory. Action cancelled.")
|
|
210
|
+
client_secret = (
|
|
211
|
+
await inquirer.text( # type: ignore
|
|
212
|
+
message="Enter Client Secret (optional):"
|
|
213
|
+
).execute_async()
|
|
214
|
+
or None
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
code_verifier = generate_token(64)
|
|
218
|
+
|
|
219
|
+
auth_url = f"{oidc['authorization_endpoint']}?{
|
|
220
|
+
urlencode(
|
|
221
|
+
{
|
|
222
|
+
'client_id': client_id,
|
|
223
|
+
'response_type': 'code',
|
|
224
|
+
'redirect_uri': REDIRECT_URI,
|
|
225
|
+
'scope': ' '.join(metadata.get('scopes_supported', ['openid', 'email', 'profile'])),
|
|
226
|
+
'code_challenge': typing.cast(str, create_s256_code_challenge(code_verifier)),
|
|
227
|
+
'code_challenge_method': 'S256',
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
}"
|
|
231
|
+
|
|
232
|
+
console.info(f"Opening browser for login: [cyan]{auth_url}[/cyan]")
|
|
233
|
+
if not webbrowser.open(auth_url):
|
|
234
|
+
console.warning("Could not open browser. Please visit the above URL manually.")
|
|
235
|
+
|
|
236
|
+
code = await _wait_for_auth_code()
|
|
237
|
+
async with httpx.AsyncClient() as client:
|
|
238
|
+
token_resp = None
|
|
239
|
+
try:
|
|
240
|
+
token_resp = await client.post(
|
|
241
|
+
oidc["token_endpoint"],
|
|
242
|
+
data={
|
|
243
|
+
"grant_type": "authorization_code",
|
|
244
|
+
"code": code,
|
|
245
|
+
"redirect_uri": REDIRECT_URI,
|
|
246
|
+
"client_id": client_id,
|
|
247
|
+
"client_secret": client_secret,
|
|
248
|
+
"code_verifier": code_verifier,
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
token_resp.raise_for_status()
|
|
252
|
+
token = token_resp.json()
|
|
253
|
+
except Exception as e:
|
|
254
|
+
if resp:
|
|
255
|
+
try:
|
|
256
|
+
error_details = resp.json()
|
|
257
|
+
console.warning(
|
|
258
|
+
f"error: {error_details['error']} error description: {error_details['error_description']}"
|
|
259
|
+
)
|
|
260
|
+
except Exception:
|
|
261
|
+
console.info("no parsable json response.")
|
|
262
|
+
|
|
263
|
+
raise RuntimeError(f"Token request failed: {e}") from e
|
|
264
|
+
|
|
265
|
+
if not token:
|
|
266
|
+
raise RuntimeError("Login timed out or not successful.")
|
|
267
|
+
|
|
268
|
+
config.auth_manager.save_auth_token(
|
|
269
|
+
server=server,
|
|
270
|
+
auth_server=auth_server,
|
|
271
|
+
client_id=client_id,
|
|
272
|
+
client_secret=client_secret,
|
|
273
|
+
token=token,
|
|
274
|
+
registration_token=registration_token,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
config.auth_manager.active_server = server
|
|
278
|
+
config.auth_manager.active_auth_server = auth_server
|
|
279
|
+
console.success(f"Logged in to [cyan]{server}[/cyan].")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@app.command("logout | remove | rm | delete")
|
|
283
|
+
async def server_logout(
|
|
284
|
+
all: typing.Annotated[
|
|
285
|
+
bool,
|
|
286
|
+
typer.Option(),
|
|
287
|
+
] = False,
|
|
288
|
+
):
|
|
289
|
+
await config.auth_manager.clear_auth_token(all=all)
|
|
290
|
+
console.success("You have been logged out.")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.command("show")
|
|
294
|
+
def server_show():
|
|
295
|
+
if not config.auth_manager.active_server:
|
|
296
|
+
console.info("No server selected.")
|
|
297
|
+
console.hint(
|
|
298
|
+
"Run [green]agentstack server list[/green] to list available servers, and [green]agentstack server login[/green] to select one."
|
|
299
|
+
)
|
|
300
|
+
return
|
|
301
|
+
console.info(f"Active server: [cyan]{config.auth_manager.active_server}[/cyan]")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@app.command("list")
|
|
305
|
+
def server_list():
|
|
306
|
+
if not config.auth_manager.servers:
|
|
307
|
+
console.info("No servers found.")
|
|
308
|
+
console.hint(
|
|
309
|
+
"Run [green]agentstack platform start[/green] to start a local server, or [green]agentstack server login[/green] to connect to a remote one."
|
|
310
|
+
)
|
|
311
|
+
return
|
|
312
|
+
for server in config.auth_manager.servers:
|
|
313
|
+
console.print(
|
|
314
|
+
f"[cyan]{server}[/cyan] {'[green](active)[/green]' if server == config.auth_manager.active_server else ''}"
|
|
315
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import typing
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from agentstack_sdk.platform import User
|
|
9
|
+
from agentstack_sdk.platform.user import UserRole
|
|
10
|
+
from rich.table import Column
|
|
11
|
+
|
|
12
|
+
from agentstack_cli.async_typer import AsyncTyper, console, create_table
|
|
13
|
+
from agentstack_cli.configuration import Configuration
|
|
14
|
+
from agentstack_cli.utils import announce_server_action, confirm_server_action
|
|
15
|
+
|
|
16
|
+
app = AsyncTyper()
|
|
17
|
+
configuration = Configuration()
|
|
18
|
+
|
|
19
|
+
ROLE_DISPLAY = {
|
|
20
|
+
"admin": "[red]admin[/red]",
|
|
21
|
+
"developer": "[cyan]developer[/cyan]",
|
|
22
|
+
"user": "user",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("list", help="List platform users [Admin only]")
|
|
27
|
+
async def list_users(
|
|
28
|
+
email: typing.Annotated[str | None, typer.Option(help="Filter by email (case-insensitive partial match)")] = None,
|
|
29
|
+
limit: typing.Annotated[int, typer.Option(help="Results per page (1-100)")] = 40,
|
|
30
|
+
after: typing.Annotated[str | None, typer.Option(help="Pagination cursor (page_token)")] = None,
|
|
31
|
+
):
|
|
32
|
+
announce_server_action("Listing users on")
|
|
33
|
+
|
|
34
|
+
async with configuration.use_platform_client():
|
|
35
|
+
result = await User.list(email=email, limit=limit, page_token=after)
|
|
36
|
+
|
|
37
|
+
items = result.items
|
|
38
|
+
has_more = result.has_more
|
|
39
|
+
next_page_token = result.next_page_token
|
|
40
|
+
|
|
41
|
+
with create_table(
|
|
42
|
+
Column("ID", style="yellow"),
|
|
43
|
+
Column("Email"),
|
|
44
|
+
Column("Role"),
|
|
45
|
+
Column("Created"),
|
|
46
|
+
no_wrap=True,
|
|
47
|
+
) as table:
|
|
48
|
+
for user in items:
|
|
49
|
+
role_display = ROLE_DISPLAY.get(user.role, user.role)
|
|
50
|
+
|
|
51
|
+
created_at = _format_date(user.created_at)
|
|
52
|
+
|
|
53
|
+
table.add_row(
|
|
54
|
+
user.id,
|
|
55
|
+
user.email,
|
|
56
|
+
role_display,
|
|
57
|
+
created_at,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
console.print()
|
|
61
|
+
console.print(table)
|
|
62
|
+
|
|
63
|
+
if has_more and next_page_token:
|
|
64
|
+
console.print(f"\n[dim]Use --after {next_page_token} to see more[/dim]")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command("set-role", help="Change user role [Admin only]")
|
|
68
|
+
async def set_role(
|
|
69
|
+
user_id: typing.Annotated[str, typer.Argument(help="User UUID")],
|
|
70
|
+
role: typing.Annotated[UserRole, typer.Argument(help="Target role (admin, developer, user)")],
|
|
71
|
+
yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
|
|
72
|
+
):
|
|
73
|
+
url = announce_server_action(f"Changing user {user_id} to role '{role}' on")
|
|
74
|
+
await confirm_server_action("Proceed with role change on", url=url, yes=yes)
|
|
75
|
+
|
|
76
|
+
async with configuration.use_platform_client():
|
|
77
|
+
result = await User.set_role(user_id, UserRole(role))
|
|
78
|
+
|
|
79
|
+
role_display = ROLE_DISPLAY.get(result.new_role, result.new_role)
|
|
80
|
+
|
|
81
|
+
console.success(f"User role updated to [cyan]{role_display}[/cyan]")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _format_date(dt: datetime | None) -> str:
|
|
85
|
+
if not dt:
|
|
86
|
+
return "-"
|
|
87
|
+
return dt.strftime("%Y-%m-%d %H:%M")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import functools
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
import pathlib
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import typing
|
|
10
|
+
from collections.abc import AsyncIterator
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
|
|
13
|
+
import pydantic
|
|
14
|
+
import pydantic_settings
|
|
15
|
+
from agentstack_sdk.platform import PlatformClient, use_platform_client
|
|
16
|
+
from pydantic import HttpUrl, SecretStr
|
|
17
|
+
|
|
18
|
+
from agentstack_cli.auth_manager import AuthManager
|
|
19
|
+
from agentstack_cli.console import console
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@functools.cache
|
|
23
|
+
def version():
|
|
24
|
+
# Python strips '-', we need to re-insert it: 1.2.3rc1 -> 1.2.3-rc1
|
|
25
|
+
return re.sub(r"([0-9])([a-z])", r"\1-\2", importlib.metadata.version("agentstack-cli"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@functools.cache
|
|
29
|
+
class Configuration(pydantic_settings.BaseSettings):
|
|
30
|
+
model_config = pydantic_settings.SettingsConfigDict(
|
|
31
|
+
env_file=None, env_prefix="AGENTSTACK__", env_nested_delimiter="__", extra="allow"
|
|
32
|
+
)
|
|
33
|
+
debug: bool = False
|
|
34
|
+
home: pathlib.Path = pydantic.Field(default_factory=lambda: pathlib.Path.home() / ".agentstack")
|
|
35
|
+
agent_registry: pydantic.AnyUrl = HttpUrl(
|
|
36
|
+
f"https://github.com/i-am-bee/agentstack@v{version()}#path=agent-registry.yaml"
|
|
37
|
+
)
|
|
38
|
+
username: str = "admin"
|
|
39
|
+
password: SecretStr | None = None
|
|
40
|
+
server_metadata_ttl: int = 86400
|
|
41
|
+
|
|
42
|
+
oidc_enabled: bool = False
|
|
43
|
+
client_id: str | None = None
|
|
44
|
+
client_secret: str | None = None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def lima_home(self) -> pathlib.Path:
|
|
48
|
+
return self.home / "lima"
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def auth_file(self) -> pathlib.Path:
|
|
52
|
+
"""Return auth config file path"""
|
|
53
|
+
return self.home / "auth.json"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def auth_manager(self) -> AuthManager:
|
|
57
|
+
return AuthManager(self.auth_file)
|
|
58
|
+
|
|
59
|
+
@asynccontextmanager
|
|
60
|
+
async def use_platform_client(self) -> AsyncIterator[PlatformClient]:
|
|
61
|
+
if self.auth_manager.active_server is None:
|
|
62
|
+
console.error("No server selected.")
|
|
63
|
+
console.hint(
|
|
64
|
+
"Run [green]agentstack platform start[/green] to start a local server, or [green]agentstack server login[/green] to connect to a remote one."
|
|
65
|
+
)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
async with use_platform_client(
|
|
68
|
+
auth=(self.username, self.password.get_secret_value()) if self.password else None,
|
|
69
|
+
auth_token=await self.auth_manager.load_auth_token(),
|
|
70
|
+
base_url=self.auth_manager.active_server + "/",
|
|
71
|
+
) as client:
|
|
72
|
+
yield client
|
|
73
|
+
|
|
74
|
+
@pydantic.model_validator(mode="after")
|
|
75
|
+
def _check_old_home(self) -> typing.Self:
|
|
76
|
+
old_home = pathlib.Path.home() / ".beeai"
|
|
77
|
+
if old_home.exists() and not self.home.exists():
|
|
78
|
+
old_home.rename(self.home)
|
|
79
|
+
return self
|