agentstack-cli 0.4.2__tar.gz → 0.4.2rc3__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.4.2 → agentstack_cli-0.4.2rc3}/PKG-INFO +1 -1
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/pyproject.toml +1 -1
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/__init__.py +4 -7
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/api.py +3 -3
- agentstack_cli-0.4.2rc3/src/agentstack_cli/auth_manager.py +126 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/agent.py +40 -58
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/build.py +47 -60
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/model.py +1 -1
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/__init__.py +5 -5
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/base_driver.py +0 -2
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/self.py +4 -8
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/server.py +4 -39
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/configuration.py +1 -1
- agentstack_cli-0.4.2rc3/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/utils.py +4 -27
- agentstack_cli-0.4.2/src/agentstack_cli/auth_manager.py +0 -241
- agentstack_cli-0.4.2/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/README.md +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/async_typer.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/__init__.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/mcp.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/istio.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/lima_driver.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/wsl_driver.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/console.py +0 -0
- {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/data/.gitignore +0 -0
|
@@ -33,14 +33,13 @@ Usage: agentstack [OPTIONS] COMMAND [ARGS]...
|
|
|
33
33
|
╰────────────────────────────────────────────────────────────────────────────╯
|
|
34
34
|
|
|
35
35
|
╭─ Agent Management ─────────────────────────────────────────────────────────╮
|
|
36
|
-
│ add Install an agent (Docker, GitHub)
|
|
36
|
+
│ add Install an agent (Docker, GitHub, local) │
|
|
37
37
|
│ remove Uninstall an agent │
|
|
38
|
-
│ update Update an agent │
|
|
39
38
|
│ info Show agent details │
|
|
40
39
|
│ logs Stream agent execution logs │
|
|
40
|
+
│ build Build an agent container image │
|
|
41
41
|
│ env Manage agent environment variables │
|
|
42
|
-
│ build
|
|
43
|
-
│ client-side-build Build an agent container image locally │
|
|
42
|
+
│ server-side-build [EXPERIMENTAL] Build agents remotely │
|
|
44
43
|
╰────────────────────────────────────────────────────────────────────────────╯
|
|
45
44
|
|
|
46
45
|
╭─ Platform & Configuration ─────────────────────────────────────────────────╮
|
|
@@ -93,9 +92,7 @@ app.add_typer(agent_alias, name="", no_args_is_help=True)
|
|
|
93
92
|
|
|
94
93
|
|
|
95
94
|
@app.command("version")
|
|
96
|
-
async def version(
|
|
97
|
-
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
98
|
-
):
|
|
95
|
+
async def version(verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False):
|
|
99
96
|
"""Print version of the Agent Stack CLI."""
|
|
100
97
|
import agentstack_cli.commands.self
|
|
101
98
|
|
|
@@ -44,7 +44,7 @@ async def api_request(
|
|
|
44
44
|
timeout=60,
|
|
45
45
|
headers=(
|
|
46
46
|
{"Authorization": f"Bearer {token}"}
|
|
47
|
-
if use_auth and (token :=
|
|
47
|
+
if use_auth and (token := config.auth_manager.load_auth_token())
|
|
48
48
|
else {}
|
|
49
49
|
),
|
|
50
50
|
)
|
|
@@ -83,7 +83,7 @@ async def api_stream(
|
|
|
83
83
|
timeout=timedelta(hours=1).total_seconds(),
|
|
84
84
|
headers=(
|
|
85
85
|
{"Authorization": f"Bearer {token}"}
|
|
86
|
-
if use_auth and (token :=
|
|
86
|
+
if use_auth and (token := config.auth_manager.load_auth_token())
|
|
87
87
|
else {}
|
|
88
88
|
),
|
|
89
89
|
) as response,
|
|
@@ -108,7 +108,7 @@ async def a2a_client(agent_card: AgentCard, use_auth: bool = True) -> AsyncItera
|
|
|
108
108
|
async with httpx.AsyncClient(
|
|
109
109
|
headers=(
|
|
110
110
|
{"Authorization": f"Bearer {token}"}
|
|
111
|
-
if use_auth and (token :=
|
|
111
|
+
if use_auth and (token := config.auth_manager.load_auth_token())
|
|
112
112
|
else {}
|
|
113
113
|
),
|
|
114
114
|
follow_redirects=True,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import pathlib
|
|
5
|
+
import typing
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthToken(BaseModel):
|
|
13
|
+
access_token: str
|
|
14
|
+
token_type: str = "Bearer"
|
|
15
|
+
expires_in: int | None = None
|
|
16
|
+
refresh_token: str | None = None
|
|
17
|
+
scope: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuthServer(BaseModel):
|
|
21
|
+
client_id: str = "df82a687-d647-4247-838b-7080d7d83f6c" # Backwards compatibility default
|
|
22
|
+
client_secret: str | None = None
|
|
23
|
+
token: AuthToken | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Server(BaseModel):
|
|
27
|
+
authorization_servers: dict[str, AuthServer] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Auth(BaseModel):
|
|
31
|
+
version: typing.Literal[1] = 1
|
|
32
|
+
servers: defaultdict[str, typing.Annotated[Server, Field(default_factory=Server)]] = Field(
|
|
33
|
+
default_factory=lambda: defaultdict(Server)
|
|
34
|
+
)
|
|
35
|
+
active_server: str | None = None
|
|
36
|
+
active_auth_server: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@typing.final
|
|
40
|
+
class AuthManager:
|
|
41
|
+
def __init__(self, config_path: pathlib.Path):
|
|
42
|
+
self._auth_path = config_path
|
|
43
|
+
self._auth = self._load()
|
|
44
|
+
|
|
45
|
+
def _load(self) -> Auth:
|
|
46
|
+
if not self._auth_path.exists():
|
|
47
|
+
return Auth()
|
|
48
|
+
return Auth.model_validate_json(self._auth_path.read_bytes())
|
|
49
|
+
|
|
50
|
+
def _save(self) -> None:
|
|
51
|
+
self._auth_path.write_text(self._auth.model_dump_json(indent=2))
|
|
52
|
+
|
|
53
|
+
def save_auth_token(
|
|
54
|
+
self,
|
|
55
|
+
server: str,
|
|
56
|
+
auth_server: str | None = None,
|
|
57
|
+
client_id: str | None = None,
|
|
58
|
+
client_secret: str | None = None,
|
|
59
|
+
token: dict[str, Any] | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
if auth_server is not None and client_id is not None and token is not None:
|
|
62
|
+
self._auth.servers[server].authorization_servers[auth_server] = AuthServer(
|
|
63
|
+
client_id=client_id, client_secret=client_secret, token=AuthToken(**token)
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
self._auth.servers[server] # touch
|
|
67
|
+
self._save()
|
|
68
|
+
|
|
69
|
+
def load_auth_token(self) -> str | None:
|
|
70
|
+
active_res = self._auth.active_server
|
|
71
|
+
active_auth_server = self._auth.active_auth_server
|
|
72
|
+
if not active_res or not active_auth_server:
|
|
73
|
+
return None
|
|
74
|
+
server = self._auth.servers.get(active_res)
|
|
75
|
+
if not server:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
auth_server = server.authorization_servers.get(active_auth_server)
|
|
79
|
+
if not auth_server or not auth_server.token:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
return auth_server.token.access_token
|
|
83
|
+
|
|
84
|
+
def clear_auth_token(self, all: bool = False) -> None:
|
|
85
|
+
if all:
|
|
86
|
+
self._auth.servers = defaultdict(Server)
|
|
87
|
+
else:
|
|
88
|
+
if self._auth.active_server and self._auth.active_auth_server:
|
|
89
|
+
del self._auth.servers[self._auth.active_server].authorization_servers[self._auth.active_auth_server]
|
|
90
|
+
if self._auth.active_server and not self._auth.servers[self._auth.active_server].authorization_servers:
|
|
91
|
+
del self._auth.servers[self._auth.active_server]
|
|
92
|
+
self._auth.active_server = None
|
|
93
|
+
self._auth.active_auth_server = None
|
|
94
|
+
self._save()
|
|
95
|
+
|
|
96
|
+
def get_server(self, server: str) -> Server | None:
|
|
97
|
+
return self._auth.servers.get(server)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def servers(self) -> list[str]:
|
|
101
|
+
return list(self._auth.servers.keys())
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def active_server(self) -> str | None:
|
|
105
|
+
return self._auth.active_server
|
|
106
|
+
|
|
107
|
+
@active_server.setter
|
|
108
|
+
def active_server(self, server: str | None) -> None:
|
|
109
|
+
if server is not None and server not in self._auth.servers:
|
|
110
|
+
raise ValueError(f"Server {server} not found")
|
|
111
|
+
self._auth.active_server = server
|
|
112
|
+
self._save()
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def active_auth_server(self) -> str | None:
|
|
116
|
+
return self._auth.active_auth_server
|
|
117
|
+
|
|
118
|
+
@active_auth_server.setter
|
|
119
|
+
def active_auth_server(self, auth_server: str | None) -> None:
|
|
120
|
+
if auth_server is not None and (
|
|
121
|
+
self._auth.active_server not in self._auth.servers
|
|
122
|
+
or auth_server not in self._auth.servers[self._auth.active_server].authorization_servers
|
|
123
|
+
):
|
|
124
|
+
raise ValueError(f"Auth server {auth_server} not found in active server")
|
|
125
|
+
self._auth.active_auth_server = auth_server
|
|
126
|
+
self._save()
|
|
@@ -61,7 +61,7 @@ from agentstack_sdk.a2a.extensions.common.form import (
|
|
|
61
61
|
TextField,
|
|
62
62
|
TextFieldValue,
|
|
63
63
|
)
|
|
64
|
-
from agentstack_sdk.platform import
|
|
64
|
+
from agentstack_sdk.platform import ModelProvider, Provider
|
|
65
65
|
from agentstack_sdk.platform.context import Context, ContextPermissions, ContextToken, Permissions
|
|
66
66
|
from agentstack_sdk.platform.model_provider import ModelCapability
|
|
67
67
|
from InquirerPy import inquirer
|
|
@@ -73,7 +73,7 @@ from rich.console import ConsoleRenderable, Group, NewLine
|
|
|
73
73
|
from rich.panel import Panel
|
|
74
74
|
from rich.text import Text
|
|
75
75
|
|
|
76
|
-
from agentstack_cli.commands.build import
|
|
76
|
+
from agentstack_cli.commands.build import build
|
|
77
77
|
from agentstack_cli.commands.model import ensure_llm_provider
|
|
78
78
|
from agentstack_cli.configuration import Configuration
|
|
79
79
|
|
|
@@ -87,6 +87,7 @@ if sys.platform != "win32":
|
|
|
87
87
|
from collections.abc import Callable
|
|
88
88
|
from pathlib import Path
|
|
89
89
|
from typing import Any
|
|
90
|
+
from urllib.parse import urlparse
|
|
90
91
|
|
|
91
92
|
import jsonschema
|
|
92
93
|
import rich.json
|
|
@@ -100,11 +101,11 @@ from agentstack_cli.utils import (
|
|
|
100
101
|
announce_server_action,
|
|
101
102
|
confirm_server_action,
|
|
102
103
|
generate_schema_example,
|
|
103
|
-
is_github_url,
|
|
104
104
|
parse_env_var,
|
|
105
105
|
print_log,
|
|
106
106
|
prompt_user,
|
|
107
107
|
remove_nullable,
|
|
108
|
+
run_command,
|
|
108
109
|
status,
|
|
109
110
|
verbosity,
|
|
110
111
|
)
|
|
@@ -152,67 +153,49 @@ configuration = Configuration()
|
|
|
152
153
|
|
|
153
154
|
@app.command("add")
|
|
154
155
|
async def add_agent(
|
|
155
|
-
location: typing.Annotated[
|
|
156
|
+
location: typing.Annotated[
|
|
157
|
+
str, typer.Argument(help="Agent location (public docker image, local path or github url)")
|
|
158
|
+
],
|
|
156
159
|
dockerfile: typing.Annotated[str | None, typer.Option(help="Use custom dockerfile path")] = None,
|
|
157
|
-
|
|
160
|
+
vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
|
|
161
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
158
162
|
yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
|
|
159
163
|
) -> None:
|
|
160
|
-
"""
|
|
164
|
+
"""Install discovered agent or add public docker image or github repository [aliases: install]"""
|
|
161
165
|
url = announce_server_action(f"Installing agent '{location}' for")
|
|
162
166
|
await confirm_server_action("Proceed with installing this agent on", url=url, yes=yes)
|
|
167
|
+
agent_card = None
|
|
163
168
|
with verbosity(verbose):
|
|
164
|
-
if
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
if (
|
|
170
|
+
process := await run_command(
|
|
171
|
+
["docker", "inspect", location], check=False, message="Inspecting docker images"
|
|
172
|
+
)
|
|
173
|
+
).returncode == 0:
|
|
174
|
+
console.success(f"Found local image [bold]{location}[/bold]")
|
|
175
|
+
manifest = base64.b64decode(
|
|
176
|
+
json.loads(process.stdout)[0]["Config"]["Labels"]["beeai.dev.agent.json"]
|
|
177
|
+
).decode()
|
|
178
|
+
agent_card = json.loads(manifest)
|
|
179
|
+
elif (
|
|
180
|
+
Path(location).expanduser().exists()
|
|
181
|
+
or location.startswith("git@")
|
|
182
|
+
or location.startswith("github.com/")
|
|
183
|
+
or location.startswith("www.github.com/")
|
|
184
|
+
or location.endswith(".git")
|
|
185
|
+
or ((u := urlparse(location)).scheme.startswith("http") and u.netloc.endswith("github.com"))
|
|
186
|
+
or u.scheme in {"ssh", "git", "git+ssh"}
|
|
187
|
+
):
|
|
188
|
+
console.info(f"Assuming build context, attempting to build agent from [bold]{location}[/bold]")
|
|
189
|
+
location, agent_card = await build(location, dockerfile, tag=None, vm_name=vm_name, import_image=True)
|
|
171
190
|
else:
|
|
172
|
-
|
|
173
|
-
raise ValueError("Dockerfile can be specified only if location is a GitHub url")
|
|
174
|
-
console.info(f"Assuming public docker image or network address, attempting to add {location}")
|
|
175
|
-
with status("Registering agent to platform"):
|
|
176
|
-
async with configuration.use_platform_client():
|
|
177
|
-
await Provider.create(location=location)
|
|
178
|
-
console.success(f"Agent [bold]{location}[/bold] added to platform")
|
|
179
|
-
await list_agents()
|
|
191
|
+
console.info(f"Assuming public docker image, attempting to pull {location}")
|
|
180
192
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
],
|
|
187
|
-
location: typing.Annotated[str, typer.Argument(help="Agent location (public docker image or github url)")],
|
|
188
|
-
dockerfile: typing.Annotated[str | None, typer.Option(help="Use custom dockerfile path")] = None,
|
|
189
|
-
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
190
|
-
yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
|
|
191
|
-
) -> None:
|
|
192
|
-
"""Upgrade agent to a newer docker image or build from GitHub repository"""
|
|
193
|
-
with verbosity(verbose):
|
|
194
|
-
async with configuration.use_platform_client():
|
|
195
|
-
provider = select_provider(search_path, providers=await Provider.list())
|
|
196
|
-
|
|
197
|
-
url = announce_server_action(f"Upgrading agent from '{provider.source}' to {location}")
|
|
198
|
-
await confirm_server_action("Proceed with upgrading agent on", url=url, yes=yes)
|
|
199
|
-
|
|
200
|
-
if is_github_url(location):
|
|
201
|
-
console.info(f"Assuming GitHub repository, attempting to build agent from [bold]{location}[/bold]")
|
|
202
|
-
with status("Building agent"):
|
|
203
|
-
build = await _server_side_build(
|
|
204
|
-
github_url=location, dockerfile=dockerfile, replace=provider.id, verbose=verbose
|
|
193
|
+
with status("Registering agent to platform"):
|
|
194
|
+
async with configuration.use_platform_client():
|
|
195
|
+
await Provider.create(
|
|
196
|
+
location=location,
|
|
197
|
+
agent_card=AgentCard.model_validate(agent_card) if agent_card else None,
|
|
205
198
|
)
|
|
206
|
-
if build.status != BuildState.COMPLETED:
|
|
207
|
-
error = build.error_message or "see logs above for details"
|
|
208
|
-
raise RuntimeError(f"Agent build failed: {error}")
|
|
209
|
-
else:
|
|
210
|
-
if dockerfile:
|
|
211
|
-
raise ValueError("Dockerfile can be specified only if location is a GitHub url")
|
|
212
|
-
console.info(f"Assuming public docker image or network address, attempting to add {location}")
|
|
213
|
-
with status("Upgrading agent in the platform"):
|
|
214
|
-
async with configuration.use_platform_client():
|
|
215
|
-
await provider.patch(location=location)
|
|
216
199
|
console.success(f"Agent [bold]{location}[/bold] added to platform")
|
|
217
200
|
await list_agents()
|
|
218
201
|
|
|
@@ -535,8 +518,7 @@ async def _run_agent(
|
|
|
535
518
|
error = ""
|
|
536
519
|
if message and message.parts and isinstance(message.parts[0].root, TextPart):
|
|
537
520
|
error = message.parts[0].root.text
|
|
538
|
-
console.print(f"
|
|
539
|
-
console.print(Markdown(error))
|
|
521
|
+
console.print(f"[red]Task {status}[/red]: {error}")
|
|
540
522
|
return
|
|
541
523
|
case Task(id=task_id), TaskStatusUpdateEvent(
|
|
542
524
|
status=TaskStatus(state=TaskState.auth_required, message=message)
|
|
@@ -796,7 +778,7 @@ async def run_agent(
|
|
|
796
778
|
metadata={"provider_id": provider.id, "agent_name": provider.agent_card.name},
|
|
797
779
|
)
|
|
798
780
|
context_token = await context.generate_token(
|
|
799
|
-
grant_global_permissions=Permissions(llm={"*"}, embeddings={"*"}, a2a_proxy={"*"}
|
|
781
|
+
grant_global_permissions=Permissions(llm={"*"}, embeddings={"*"}, a2a_proxy={"*"}),
|
|
800
782
|
grant_context_permissions=ContextPermissions(files={"*"}, vector_stores={"*"}, context_data={"*"}),
|
|
801
783
|
)
|
|
802
784
|
|
|
@@ -8,7 +8,6 @@ import re
|
|
|
8
8
|
import sys
|
|
9
9
|
import typing
|
|
10
10
|
import uuid
|
|
11
|
-
from asyncio import CancelledError
|
|
12
11
|
from contextlib import suppress
|
|
13
12
|
from datetime import timedelta
|
|
14
13
|
from pathlib import Path
|
|
@@ -18,13 +17,13 @@ import anyio.abc
|
|
|
18
17
|
import typer
|
|
19
18
|
from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH
|
|
20
19
|
from agentstack_sdk.platform import AddProvider, BuildConfiguration, Provider, UpdateProvider
|
|
21
|
-
from agentstack_sdk.platform.provider_build import ProviderBuild
|
|
20
|
+
from agentstack_sdk.platform.provider_build import BuildState, ProviderBuild
|
|
22
21
|
from anyio import open_process
|
|
23
22
|
from httpx import AsyncClient, HTTPError
|
|
24
23
|
from tenacity import AsyncRetrying, retry_if_exception_type, stop_after_delay, wait_fixed
|
|
25
24
|
|
|
26
25
|
from agentstack_cli.async_typer import AsyncTyper
|
|
27
|
-
from agentstack_cli.console import console
|
|
26
|
+
from agentstack_cli.console import console
|
|
28
27
|
from agentstack_cli.utils import (
|
|
29
28
|
announce_server_action,
|
|
30
29
|
capture_output,
|
|
@@ -48,8 +47,8 @@ async def find_free_port():
|
|
|
48
47
|
app = AsyncTyper()
|
|
49
48
|
|
|
50
49
|
|
|
51
|
-
@app.command("
|
|
52
|
-
async def
|
|
50
|
+
@app.command("build")
|
|
51
|
+
async def build(
|
|
53
52
|
context: typing.Annotated[str, typer.Argument(help="Docker context for the agent")] = ".",
|
|
54
53
|
dockerfile: typing.Annotated[str | None, typer.Option(help="Use custom dockerfile path")] = None,
|
|
55
54
|
tag: typing.Annotated[str | None, typer.Option(help="Docker tag for the agent")] = None,
|
|
@@ -59,9 +58,8 @@ async def client_side_build(
|
|
|
59
58
|
bool, typer.Option("--import/--no-import", is_flag=True, help="Import the image into Agent Stack platform")
|
|
60
59
|
] = True,
|
|
61
60
|
vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
|
|
62
|
-
verbose: typing.Annotated[bool, typer.Option("-v"
|
|
61
|
+
verbose: typing.Annotated[bool, typer.Option("-v")] = False,
|
|
63
62
|
):
|
|
64
|
-
"""Build agent locally using Docker."""
|
|
65
63
|
with verbosity(verbose):
|
|
66
64
|
await run_command(["which", "docker"], "Checking docker")
|
|
67
65
|
image_id = "agentstack-agent-build-tmp:latest"
|
|
@@ -144,67 +142,56 @@ async def client_side_build(
|
|
|
144
142
|
return tag, agent_card
|
|
145
143
|
|
|
146
144
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
dockerfile: str | None = None,
|
|
150
|
-
replace: str | None = None,
|
|
151
|
-
add: bool = False,
|
|
152
|
-
verbose: bool = False,
|
|
153
|
-
) -> ProviderBuild:
|
|
154
|
-
build = None
|
|
155
|
-
try:
|
|
156
|
-
from agentstack_cli.commands.agent import select_provider
|
|
157
|
-
from agentstack_cli.configuration import Configuration
|
|
158
|
-
|
|
159
|
-
if replace and add:
|
|
160
|
-
raise ValueError("Cannot specify both replace and add options.")
|
|
161
|
-
|
|
162
|
-
build_configuration = None
|
|
163
|
-
if dockerfile:
|
|
164
|
-
build_configuration = BuildConfiguration(dockerfile_path=Path(dockerfile))
|
|
165
|
-
|
|
166
|
-
async with Configuration().use_platform_client():
|
|
167
|
-
on_complete = None
|
|
168
|
-
if replace:
|
|
169
|
-
provider = select_provider(replace, await Provider.list())
|
|
170
|
-
on_complete = UpdateProvider(provider_id=uuid.UUID(provider.id))
|
|
171
|
-
elif add:
|
|
172
|
-
on_complete = AddProvider()
|
|
173
|
-
|
|
174
|
-
build = await ProviderBuild.create(
|
|
175
|
-
location=github_url,
|
|
176
|
-
on_complete=on_complete,
|
|
177
|
-
build_configuration=build_configuration,
|
|
178
|
-
)
|
|
179
|
-
with verbosity(verbose):
|
|
180
|
-
async for message in build.stream_logs():
|
|
181
|
-
print_log(message, ansi_mode=True, out_console=err_console)
|
|
182
|
-
return await build.get()
|
|
183
|
-
except (KeyboardInterrupt, CancelledError):
|
|
184
|
-
if build:
|
|
185
|
-
await build.delete()
|
|
186
|
-
console.error("Build aborted.")
|
|
187
|
-
raise
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
@app.command("build")
|
|
191
|
-
async def server_side_build(
|
|
145
|
+
@app.command("server-side-build")
|
|
146
|
+
async def server_side_build_experimental(
|
|
192
147
|
github_url: typing.Annotated[
|
|
193
148
|
str, typer.Argument(..., help="Github repository URL (public or private if supported by the platform instance)")
|
|
194
149
|
],
|
|
195
150
|
dockerfile: typing.Annotated[
|
|
196
151
|
str | None, typer.Option(help="Use custom dockerfile path, relative to github url sub-path")
|
|
197
152
|
] = None,
|
|
198
|
-
|
|
153
|
+
replace: typing.Annotated[
|
|
154
|
+
str | None, typer.Option(help="Short ID, agent name or part of the provider location")
|
|
155
|
+
] = None,
|
|
156
|
+
add: typing.Annotated[bool, typer.Option(help="Add agent to the platform after build")] = False,
|
|
199
157
|
yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
|
|
200
158
|
):
|
|
201
|
-
"""Build agent from
|
|
159
|
+
"""EXPERIMENTAL: Build agent from github repository in the platform."""
|
|
160
|
+
from agentstack_cli.commands.agent import select_provider
|
|
161
|
+
from agentstack_cli.configuration import Configuration
|
|
202
162
|
|
|
203
|
-
|
|
204
|
-
|
|
163
|
+
if replace and add:
|
|
164
|
+
raise ValueError("Cannot specify both replace and add options.")
|
|
165
|
+
|
|
166
|
+
build_configuration = None
|
|
167
|
+
if dockerfile:
|
|
168
|
+
build_configuration = BuildConfiguration(dockerfile_path=Path(dockerfile))
|
|
205
169
|
|
|
206
|
-
|
|
170
|
+
url = announce_server_action(f"Starting server-side build for '{github_url}' on")
|
|
171
|
+
await confirm_server_action("Proceed with building this agent on", url=url, yes=yes)
|
|
207
172
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
173
|
+
async with Configuration().use_platform_client():
|
|
174
|
+
on_complete = None
|
|
175
|
+
if replace:
|
|
176
|
+
provider = select_provider(replace, await Provider.list())
|
|
177
|
+
on_complete = UpdateProvider(provider_id=uuid.UUID(provider.id))
|
|
178
|
+
elif add:
|
|
179
|
+
on_complete = AddProvider()
|
|
180
|
+
|
|
181
|
+
build = await ProviderBuild.create(
|
|
182
|
+
location=github_url,
|
|
183
|
+
on_complete=on_complete,
|
|
184
|
+
build_configuration=build_configuration,
|
|
185
|
+
)
|
|
186
|
+
async for message in build.stream_logs():
|
|
187
|
+
print_log(message, ansi_mode=True)
|
|
188
|
+
build = await build.get()
|
|
189
|
+
if build.status == BuildState.COMPLETED:
|
|
190
|
+
if add:
|
|
191
|
+
message = "Agent added successfully. List agents using [green]agentstack list[/green]"
|
|
192
|
+
else:
|
|
193
|
+
message = f"Agent built successfully, add it to the platform using: [green]agentstack add {build.destination}[/green]"
|
|
194
|
+
console.success(message)
|
|
195
|
+
else:
|
|
196
|
+
error = build.error_message or "see logs above for details"
|
|
197
|
+
console.error(f"Agent build failed: {error}")
|
|
@@ -413,7 +413,7 @@ async def _reset_configuration(existing_providers: list[ModelProvider] | None =
|
|
|
413
413
|
@app.command("setup")
|
|
414
414
|
async def setup(
|
|
415
415
|
use_true_localhost: typing.Annotated[bool, typer.Option(hidden=True)] = False,
|
|
416
|
-
verbose: typing.Annotated[bool, typer.Option("-v"
|
|
416
|
+
verbose: typing.Annotated[bool, typer.Option("-v")] = False,
|
|
417
417
|
):
|
|
418
418
|
"""Interactive setup for LLM and embedding provider environment variables"""
|
|
419
419
|
announce_server_action("Configuring model providers for")
|
{agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/__init__.py
RENAMED
|
@@ -72,7 +72,7 @@ async def start(
|
|
|
72
72
|
pathlib.Path | None, typer.Option("-f", help="Set Helm chart values using yaml values file")
|
|
73
73
|
] = None,
|
|
74
74
|
vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
|
|
75
|
-
verbose: typing.Annotated[bool, typer.Option("-v",
|
|
75
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
76
76
|
):
|
|
77
77
|
"""Start Agent Stack platform."""
|
|
78
78
|
import agentstack_cli.commands.server
|
|
@@ -127,7 +127,7 @@ async def start(
|
|
|
127
127
|
@app.command("stop")
|
|
128
128
|
async def stop(
|
|
129
129
|
vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
|
|
130
|
-
verbose: typing.Annotated[bool, typer.Option("-v",
|
|
130
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
131
131
|
):
|
|
132
132
|
"""Stop Agent Stack platform."""
|
|
133
133
|
with verbosity(verbose):
|
|
@@ -142,7 +142,7 @@ async def stop(
|
|
|
142
142
|
@app.command("delete")
|
|
143
143
|
async def delete(
|
|
144
144
|
vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
|
|
145
|
-
verbose: typing.Annotated[bool, typer.Option("-v",
|
|
145
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
146
146
|
):
|
|
147
147
|
"""Delete Agent Stack platform."""
|
|
148
148
|
with verbosity(verbose):
|
|
@@ -155,7 +155,7 @@ async def delete(
|
|
|
155
155
|
async def import_image_cmd(
|
|
156
156
|
tag: typing.Annotated[str, typer.Argument(help="Docker image tag to import")],
|
|
157
157
|
vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
|
|
158
|
-
verbose: typing.Annotated[bool, typer.Option("-v",
|
|
158
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
159
159
|
):
|
|
160
160
|
"""Import a local docker image into the Agent Stack platform."""
|
|
161
161
|
with verbosity(verbose):
|
|
@@ -170,7 +170,7 @@ async def import_image_cmd(
|
|
|
170
170
|
async def exec_cmd(
|
|
171
171
|
command: typing.Annotated[list[str] | None, typer.Argument()] = None,
|
|
172
172
|
vm_name: typing.Annotated[str, typer.Option(hidden=True)] = "agentstack",
|
|
173
|
-
verbose: typing.Annotated[bool, typer.Option("-v",
|
|
173
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
174
174
|
):
|
|
175
175
|
"""For debugging -- execute a command inside the Agent Stack platform VM."""
|
|
176
176
|
with verbosity(verbose, show_success_status=False):
|
{agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/base_driver.py
RENAMED
|
@@ -117,8 +117,6 @@ class BaseDriver(abc.ABC):
|
|
|
117
117
|
"generateConversationTitle": False, # TODO: enable when UI implementation is ready
|
|
118
118
|
"uiLocalSetup": True,
|
|
119
119
|
},
|
|
120
|
-
"providerBuilds": {"enabled": True},
|
|
121
|
-
"localDockerRegistry": {"enabled": True},
|
|
122
120
|
"auth": {"enabled": False},
|
|
123
121
|
}
|
|
124
122
|
if values_file:
|
|
@@ -40,9 +40,7 @@ def _path() -> str:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
@app.command("version")
|
|
43
|
-
async def version(
|
|
44
|
-
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
45
|
-
):
|
|
43
|
+
async def version(verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False):
|
|
46
44
|
"""Print version of the Agent Stack CLI."""
|
|
47
45
|
with verbosity(verbose=verbose):
|
|
48
46
|
cli_version = importlib.metadata.version("agentstack-cli")
|
|
@@ -80,7 +78,7 @@ async def version(
|
|
|
80
78
|
|
|
81
79
|
@app.command("install")
|
|
82
80
|
async def install(
|
|
83
|
-
verbose: typing.Annotated[bool, typer.Option("-v",
|
|
81
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
84
82
|
):
|
|
85
83
|
"""Install Agent Stack platform pre-requisites."""
|
|
86
84
|
with verbosity(verbose=verbose):
|
|
@@ -173,9 +171,7 @@ async def install(
|
|
|
173
171
|
|
|
174
172
|
|
|
175
173
|
@app.command("upgrade")
|
|
176
|
-
async def upgrade(
|
|
177
|
-
verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
|
|
178
|
-
):
|
|
174
|
+
async def upgrade(verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False):
|
|
179
175
|
"""Upgrade Agent Stack CLI and Platform to the latest version."""
|
|
180
176
|
if not shutil.which("uv", path=_path()):
|
|
181
177
|
console.error("Can't self-upgrade because 'uv' was not found.")
|
|
@@ -193,7 +189,7 @@ async def upgrade(
|
|
|
193
189
|
|
|
194
190
|
@app.command("uninstall")
|
|
195
191
|
async def uninstall(
|
|
196
|
-
verbose: typing.Annotated[bool, typer.Option("-v",
|
|
192
|
+
verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False,
|
|
197
193
|
):
|
|
198
194
|
"""Uninstall Agent Stack CLI and Platform."""
|
|
199
195
|
if not shutil.which("uv", path=_path()):
|
|
@@ -5,7 +5,6 @@ import asyncio
|
|
|
5
5
|
import logging
|
|
6
6
|
import sys
|
|
7
7
|
import typing
|
|
8
|
-
import uuid
|
|
9
8
|
import webbrowser
|
|
10
9
|
from urllib.parse import urlencode
|
|
11
10
|
|
|
@@ -69,10 +68,6 @@ async def _wait_for_auth_code(port: int = 9001) -> str:
|
|
|
69
68
|
return code
|
|
70
69
|
|
|
71
70
|
|
|
72
|
-
def get_unique_app_name() -> str:
|
|
73
|
-
return f"Agent Stack CLI {uuid.uuid4()}"
|
|
74
|
-
|
|
75
|
-
|
|
76
71
|
@app.command("login | change | select | default | switch")
|
|
77
72
|
async def server_login(server: typing.Annotated[str | None, typer.Argument()] = None):
|
|
78
73
|
"""Login to a server or switch between logged in servers."""
|
|
@@ -142,7 +137,6 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
142
137
|
|
|
143
138
|
client_id = config.client_id
|
|
144
139
|
client_secret = config.client_secret
|
|
145
|
-
registration_token = None
|
|
146
140
|
|
|
147
141
|
if auth_servers:
|
|
148
142
|
if len(auth_servers) == 1:
|
|
@@ -167,35 +161,17 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
167
161
|
registration_endpoint = oidc["registration_endpoint"]
|
|
168
162
|
if not client_id and registration_endpoint:
|
|
169
163
|
async with httpx.AsyncClient() as client:
|
|
170
|
-
resp = None
|
|
171
164
|
try:
|
|
172
|
-
app_name = get_unique_app_name()
|
|
173
165
|
resp = await client.post(
|
|
174
166
|
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
|
-
},
|
|
167
|
+
json={"client_name": "Agent Stack CLI", "redirect_uris": [REDIRECT_URI]},
|
|
182
168
|
)
|
|
183
169
|
resp.raise_for_status()
|
|
184
170
|
data = resp.json()
|
|
185
171
|
client_id = data["client_id"]
|
|
186
172
|
client_secret = data["client_secret"]
|
|
187
|
-
|
|
188
|
-
|
|
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}")
|
|
173
|
+
except Exception:
|
|
174
|
+
console.warning("Dynamic client registration failed. Proceed with manual input.")
|
|
199
175
|
|
|
200
176
|
if not client_id:
|
|
201
177
|
client_id = await inquirer.text( # type: ignore
|
|
@@ -232,7 +208,6 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
232
208
|
|
|
233
209
|
code = await _wait_for_auth_code()
|
|
234
210
|
async with httpx.AsyncClient() as client:
|
|
235
|
-
token_resp = None
|
|
236
211
|
try:
|
|
237
212
|
token_resp = await client.post(
|
|
238
213
|
oidc["token_endpoint"],
|
|
@@ -248,15 +223,6 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
248
223
|
token_resp.raise_for_status()
|
|
249
224
|
token = token_resp.json()
|
|
250
225
|
except Exception as e:
|
|
251
|
-
if resp:
|
|
252
|
-
try:
|
|
253
|
-
error_details = resp.json()
|
|
254
|
-
console.warning(
|
|
255
|
-
f"error: {error_details['error']} error description: {error_details['error_description']}"
|
|
256
|
-
)
|
|
257
|
-
except Exception:
|
|
258
|
-
console.info("no parsable json response.")
|
|
259
|
-
|
|
260
226
|
raise RuntimeError(f"Token request failed: {e}") from e
|
|
261
227
|
|
|
262
228
|
if not token:
|
|
@@ -268,7 +234,6 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
268
234
|
client_id=client_id,
|
|
269
235
|
client_secret=client_secret,
|
|
270
236
|
token=token,
|
|
271
|
-
registration_token=registration_token,
|
|
272
237
|
)
|
|
273
238
|
|
|
274
239
|
config.auth_manager.active_server = server
|
|
@@ -283,7 +248,7 @@ async def server_logout(
|
|
|
283
248
|
typer.Option(),
|
|
284
249
|
] = False,
|
|
285
250
|
):
|
|
286
|
-
|
|
251
|
+
config.auth_manager.clear_auth_token(all=all)
|
|
287
252
|
console.success("You have been logged out.")
|
|
288
253
|
|
|
289
254
|
|
|
@@ -65,7 +65,7 @@ class Configuration(pydantic_settings.BaseSettings):
|
|
|
65
65
|
sys.exit(1)
|
|
66
66
|
async with use_platform_client(
|
|
67
67
|
auth=("admin", self.admin_password.get_secret_value()) if self.admin_password else None,
|
|
68
|
-
auth_token=
|
|
68
|
+
auth_token=self.auth_manager.load_auth_token(),
|
|
69
69
|
base_url=self.auth_manager.active_server + "/",
|
|
70
70
|
) as client:
|
|
71
71
|
yield client
|
|
Binary file
|
|
@@ -5,7 +5,6 @@ import contextlib
|
|
|
5
5
|
import functools
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
-
import re
|
|
9
8
|
import subprocess
|
|
10
9
|
import sys
|
|
11
10
|
from collections.abc import AsyncIterator
|
|
@@ -27,7 +26,7 @@ from jsf import JSF
|
|
|
27
26
|
from prompt_toolkit import PromptSession
|
|
28
27
|
from prompt_toolkit.shortcuts import CompleteStyle
|
|
29
28
|
from pydantic import BaseModel
|
|
30
|
-
from rich.console import Capture
|
|
29
|
+
from rich.console import Capture
|
|
31
30
|
from rich.text import Text
|
|
32
31
|
|
|
33
32
|
from agentstack_cli.configuration import Configuration
|
|
@@ -297,7 +296,7 @@ def verbosity(verbose: bool, show_success_status: bool = True):
|
|
|
297
296
|
SHOW_SUCCESS_STATUS.reset(token_command_status)
|
|
298
297
|
|
|
299
298
|
|
|
300
|
-
def print_log(line, ansi_mode=False
|
|
299
|
+
def print_log(line, ansi_mode=False):
|
|
301
300
|
if "error" in line:
|
|
302
301
|
|
|
303
302
|
class CustomError(Exception): ...
|
|
@@ -310,28 +309,6 @@ def print_log(line, ansi_mode=False, out_console: Console | None = None):
|
|
|
310
309
|
return Text.from_ansi(text) if ansi_mode else text
|
|
311
310
|
|
|
312
311
|
if line["stream"] == "stderr":
|
|
313
|
-
|
|
312
|
+
err_console.print(decode(line["message"]))
|
|
314
313
|
elif line["stream"] == "stdout":
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def is_github_url(url: str) -> bool:
|
|
319
|
-
"""This pattern is taken from agentstack_server.utils.github.GithubUrl, make sure to keep it in sync"""
|
|
320
|
-
|
|
321
|
-
pattern = r"""
|
|
322
|
-
^
|
|
323
|
-
(?:git\+)? # Optional git+ prefix
|
|
324
|
-
https?://(?P<host>github(?:\.[^/]+)+)/ # GitHub host (github.com or github.enterprise.com)
|
|
325
|
-
(?P<org>[^/]+)/ # Organization
|
|
326
|
-
(?P<repo>
|
|
327
|
-
(?: # Non-capturing group for repo name
|
|
328
|
-
(?!\.git(?:$|[@#])) # Negative lookahead for .git at end or followed by @#
|
|
329
|
-
[^/@#] # Any char except /@#
|
|
330
|
-
)+ # One or more of these chars
|
|
331
|
-
)
|
|
332
|
-
(?:\.git)? # Optional .git suffix
|
|
333
|
-
(?:@(?P<version>[^#]+))? # Optional version after @
|
|
334
|
-
(?:\#path=(?P<path>.+))? # Optional path after #path=
|
|
335
|
-
$
|
|
336
|
-
"""
|
|
337
|
-
return bool(re.match(pattern, url, re.VERBOSE))
|
|
314
|
+
console.print(decode(line["message"]))
|
|
@@ -1,241 +0,0 @@
|
|
|
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 pydantic import BaseModel, Field
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class AuthToken(BaseModel):
|
|
14
|
-
access_token: str
|
|
15
|
-
token_type: str = "Bearer"
|
|
16
|
-
expires_in: int | None = None
|
|
17
|
-
expires_at: int | None = None
|
|
18
|
-
refresh_token: str | None = None
|
|
19
|
-
scope: str | None = None
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AuthServer(BaseModel):
|
|
23
|
-
client_id: str = "df82a687-d647-4247-838b-7080d7d83f6c" # Backwards compatibility default
|
|
24
|
-
client_secret: str | None = None
|
|
25
|
-
token: AuthToken | None = None
|
|
26
|
-
registration_token: str | None = None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class Server(BaseModel):
|
|
30
|
-
authorization_servers: dict[str, AuthServer] = Field(default_factory=dict)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class Auth(BaseModel):
|
|
34
|
-
version: typing.Literal[1] = 1
|
|
35
|
-
servers: defaultdict[str, typing.Annotated[Server, Field(default_factory=Server)]] = Field(
|
|
36
|
-
default_factory=lambda: defaultdict(Server)
|
|
37
|
-
)
|
|
38
|
-
active_server: str | None = None
|
|
39
|
-
active_auth_server: str | None = None
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@typing.final
|
|
43
|
-
class AuthManager:
|
|
44
|
-
def __init__(self, config_path: pathlib.Path):
|
|
45
|
-
self._auth_path = config_path
|
|
46
|
-
self._auth = self._load()
|
|
47
|
-
|
|
48
|
-
def _load(self) -> Auth:
|
|
49
|
-
if not self._auth_path.exists():
|
|
50
|
-
return Auth()
|
|
51
|
-
return Auth.model_validate_json(self._auth_path.read_bytes())
|
|
52
|
-
|
|
53
|
-
def _save(self) -> None:
|
|
54
|
-
self._auth_path.write_text(self._auth.model_dump_json(indent=2))
|
|
55
|
-
|
|
56
|
-
def save_auth_token(
|
|
57
|
-
self,
|
|
58
|
-
server: str,
|
|
59
|
-
auth_server: str | None = None,
|
|
60
|
-
client_id: str | None = None,
|
|
61
|
-
client_secret: str | None = None,
|
|
62
|
-
token: dict[str, Any] | None = None,
|
|
63
|
-
registration_token: str | None = None,
|
|
64
|
-
) -> None:
|
|
65
|
-
if auth_server is not None and client_id is not None and token is not None:
|
|
66
|
-
if token["access_token"]:
|
|
67
|
-
usetimestamp = int(time.time()) + int(token["expires_in"])
|
|
68
|
-
token["expires_at"] = usetimestamp
|
|
69
|
-
self._auth.servers[server].authorization_servers[auth_server] = AuthServer(
|
|
70
|
-
client_id=client_id,
|
|
71
|
-
client_secret=client_secret,
|
|
72
|
-
token=AuthToken(**token),
|
|
73
|
-
registration_token=registration_token,
|
|
74
|
-
)
|
|
75
|
-
else:
|
|
76
|
-
self._auth.servers[server] # touch
|
|
77
|
-
self._save()
|
|
78
|
-
|
|
79
|
-
async def exchange_refresh_token(self, auth_server: str, token: AuthToken) -> dict[str, Any] | None:
|
|
80
|
-
"""
|
|
81
|
-
This method exchanges a refresh token for a new access token.
|
|
82
|
-
"""
|
|
83
|
-
async with httpx.AsyncClient(headers={"Accept": "application/json"}) as client:
|
|
84
|
-
resp = None
|
|
85
|
-
try:
|
|
86
|
-
resp = await client.get(f"{auth_server}/.well-known/openid-configuration")
|
|
87
|
-
resp.raise_for_status()
|
|
88
|
-
oidc = resp.json()
|
|
89
|
-
except Exception as e:
|
|
90
|
-
if resp:
|
|
91
|
-
error_details = resp.json()
|
|
92
|
-
print(f"error: {error_details['error']} error description: {error_details['error_description']}")
|
|
93
|
-
raise RuntimeError(f"OIDC discovery failed: {e}") from e
|
|
94
|
-
|
|
95
|
-
token_endpoint = oidc["token_endpoint"]
|
|
96
|
-
try:
|
|
97
|
-
client_id = (
|
|
98
|
-
self._auth.servers[self._auth.active_server or ""].authorization_servers[auth_server].client_id
|
|
99
|
-
)
|
|
100
|
-
client_secret = (
|
|
101
|
-
self._auth.servers[self._auth.active_server or ""].authorization_servers[auth_server].client_secret
|
|
102
|
-
)
|
|
103
|
-
resp = await client.post(
|
|
104
|
-
f"{token_endpoint}",
|
|
105
|
-
data={
|
|
106
|
-
"grant_type": "refresh_token",
|
|
107
|
-
"refresh_token": token.refresh_token,
|
|
108
|
-
"scope": token.scope,
|
|
109
|
-
"client_id": client_id,
|
|
110
|
-
"client_secret": client_secret,
|
|
111
|
-
},
|
|
112
|
-
)
|
|
113
|
-
resp.raise_for_status()
|
|
114
|
-
new_token = resp.json()
|
|
115
|
-
except Exception as e:
|
|
116
|
-
if resp:
|
|
117
|
-
error_details = resp.json()
|
|
118
|
-
print(f"error: {error_details['error']} error description: {error_details['error_description']}")
|
|
119
|
-
raise RuntimeError(f"Failed to refresh token: {e}") from e
|
|
120
|
-
self.save_auth_token(
|
|
121
|
-
self._auth.active_server or "",
|
|
122
|
-
self._auth.active_auth_server or "",
|
|
123
|
-
self._auth.servers[self._auth.active_server or ""].authorization_servers[auth_server].client_id or "",
|
|
124
|
-
self._auth.servers[self._auth.active_server or ""].authorization_servers[auth_server].client_secret
|
|
125
|
-
or "",
|
|
126
|
-
token=new_token,
|
|
127
|
-
)
|
|
128
|
-
return new_token
|
|
129
|
-
|
|
130
|
-
async def load_auth_token(self) -> str | None:
|
|
131
|
-
active_res = self._auth.active_server
|
|
132
|
-
active_auth_server = self._auth.active_auth_server
|
|
133
|
-
if not active_res or not active_auth_server:
|
|
134
|
-
return None
|
|
135
|
-
server = self._auth.servers.get(active_res)
|
|
136
|
-
if not server:
|
|
137
|
-
return None
|
|
138
|
-
|
|
139
|
-
auth_server = server.authorization_servers.get(active_auth_server)
|
|
140
|
-
if not auth_server or not auth_server.token:
|
|
141
|
-
return None
|
|
142
|
-
|
|
143
|
-
if (auth_server.token.expires_at or 0) - 60 < time.time():
|
|
144
|
-
new_token = await self.exchange_refresh_token(active_auth_server, auth_server.token)
|
|
145
|
-
if new_token:
|
|
146
|
-
return new_token["access_token"]
|
|
147
|
-
return None
|
|
148
|
-
|
|
149
|
-
return auth_server.token.access_token
|
|
150
|
-
|
|
151
|
-
async def deregister_client(self, auth_server, client_id, registration_token) -> None:
|
|
152
|
-
async with httpx.AsyncClient(headers={"Accept": "application/json"}) as client:
|
|
153
|
-
resp = None
|
|
154
|
-
try:
|
|
155
|
-
resp = await client.get(f"{auth_server}/.well-known/openid-configuration")
|
|
156
|
-
resp.raise_for_status()
|
|
157
|
-
oidc = resp.json()
|
|
158
|
-
registration_endpoint = oidc["registration_endpoint"]
|
|
159
|
-
except Exception as e:
|
|
160
|
-
if resp:
|
|
161
|
-
error_details = resp.json()
|
|
162
|
-
print(f"error: {error_details['error']} error description: {error_details['error_description']}")
|
|
163
|
-
raise RuntimeError(f"OIDC discovery failed: {e}") from e
|
|
164
|
-
|
|
165
|
-
try:
|
|
166
|
-
if client_id is not None and client_id != "" and registration_token is not None:
|
|
167
|
-
headers = {"authorization": f"bearer {registration_token}"}
|
|
168
|
-
resp = await client.delete(f"{registration_endpoint}/{client_id}", headers=headers)
|
|
169
|
-
resp.raise_for_status()
|
|
170
|
-
|
|
171
|
-
except Exception as e:
|
|
172
|
-
if resp:
|
|
173
|
-
error_details = resp.json()
|
|
174
|
-
print(f"error: {error_details['error']} error description: {error_details['error_description']}")
|
|
175
|
-
raise RuntimeError(f"Dynamic client de-registration failed. {e}") from e
|
|
176
|
-
|
|
177
|
-
async def clear_auth_token(self, all: bool = False) -> None:
|
|
178
|
-
if all:
|
|
179
|
-
for server in self._auth.servers:
|
|
180
|
-
for auth_server in self._auth.servers[server].authorization_servers:
|
|
181
|
-
await self.deregister_client(
|
|
182
|
-
auth_server,
|
|
183
|
-
self._auth.servers[server].authorization_servers[auth_server].client_id,
|
|
184
|
-
self._auth.servers[server].authorization_servers[auth_server].registration_token,
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
self._auth.servers = defaultdict(Server)
|
|
188
|
-
else:
|
|
189
|
-
if self._auth.active_server and self._auth.active_auth_server:
|
|
190
|
-
if (
|
|
191
|
-
self._auth.servers[self._auth.active_server]
|
|
192
|
-
.authorization_servers[self._auth.active_auth_server]
|
|
193
|
-
.client_id
|
|
194
|
-
):
|
|
195
|
-
await self.deregister_client(
|
|
196
|
-
self._auth.active_auth_server,
|
|
197
|
-
self._auth.servers[self._auth.active_server]
|
|
198
|
-
.authorization_servers[self._auth.active_auth_server]
|
|
199
|
-
.client_id,
|
|
200
|
-
self._auth.servers[self._auth.active_server]
|
|
201
|
-
.authorization_servers[self._auth.active_auth_server]
|
|
202
|
-
.registration_token,
|
|
203
|
-
)
|
|
204
|
-
del self._auth.servers[self._auth.active_server].authorization_servers[self._auth.active_auth_server]
|
|
205
|
-
if self._auth.active_server and not self._auth.servers[self._auth.active_server].authorization_servers:
|
|
206
|
-
del self._auth.servers[self._auth.active_server]
|
|
207
|
-
self._auth.active_server = None
|
|
208
|
-
self._auth.active_auth_server = None
|
|
209
|
-
self._save()
|
|
210
|
-
|
|
211
|
-
def get_server(self, server: str) -> Server | None:
|
|
212
|
-
return self._auth.servers.get(server)
|
|
213
|
-
|
|
214
|
-
@property
|
|
215
|
-
def servers(self) -> list[str]:
|
|
216
|
-
return list(self._auth.servers.keys())
|
|
217
|
-
|
|
218
|
-
@property
|
|
219
|
-
def active_server(self) -> str | None:
|
|
220
|
-
return self._auth.active_server
|
|
221
|
-
|
|
222
|
-
@active_server.setter
|
|
223
|
-
def active_server(self, server: str | None) -> None:
|
|
224
|
-
if server is not None and server not in self._auth.servers:
|
|
225
|
-
raise ValueError(f"Server {server} not found")
|
|
226
|
-
self._auth.active_server = server
|
|
227
|
-
self._save()
|
|
228
|
-
|
|
229
|
-
@property
|
|
230
|
-
def active_auth_server(self) -> str | None:
|
|
231
|
-
return self._auth.active_auth_server
|
|
232
|
-
|
|
233
|
-
@active_auth_server.setter
|
|
234
|
-
def active_auth_server(self, auth_server: str | None) -> None:
|
|
235
|
-
if auth_server is not None and (
|
|
236
|
-
self._auth.active_server not in self._auth.servers
|
|
237
|
-
or auth_server not in self._auth.servers[self._auth.active_server].authorization_servers
|
|
238
|
-
):
|
|
239
|
-
raise ValueError(f"Auth server {auth_server} not found in active server")
|
|
240
|
-
self._auth.active_auth_server = auth_server
|
|
241
|
-
self._save()
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/istio.py
RENAMED
|
File without changes
|
{agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/lima_driver.py
RENAMED
|
File without changes
|
{agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc3}/src/agentstack_cli/commands/platform/wsl_driver.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|