agentstack-cli 0.4.2rc9__tar.gz → 0.4.3__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.2rc9 → agentstack_cli-0.4.3}/PKG-INFO +1 -1
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/pyproject.toml +1 -1
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/api.py +3 -3
- agentstack_cli-0.4.3/src/agentstack_cli/auth_manager.py +241 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/agent.py +2 -1
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/base_driver.py +1 -5
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/server.py +39 -4
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/configuration.py +1 -1
- agentstack_cli-0.4.3/src/agentstack_cli/data/helm-chart.tgz +0 -0
- agentstack_cli-0.4.2rc9/src/agentstack_cli/auth_manager.py +0 -126
- agentstack_cli-0.4.2rc9/src/agentstack_cli/data/helm-chart.tgz +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/README.md +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/__init__.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/async_typer.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/__init__.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/build.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/mcp.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/model.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/__init__.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/istio.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/lima_driver.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/wsl_driver.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/self.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/console.py +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/data/.gitignore +0 -0
- {agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/utils.py +0 -0
|
@@ -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 := config.auth_manager.load_auth_token())
|
|
47
|
+
if use_auth and (token := await 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 := config.auth_manager.load_auth_token())
|
|
86
|
+
if use_auth and (token := await 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 := config.auth_manager.load_auth_token())
|
|
111
|
+
if use_auth and (token := await config.auth_manager.load_auth_token())
|
|
112
112
|
else {}
|
|
113
113
|
),
|
|
114
114
|
follow_redirects=True,
|
|
@@ -0,0 +1,241 @@
|
|
|
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()
|
|
@@ -535,7 +535,8 @@ async def _run_agent(
|
|
|
535
535
|
error = ""
|
|
536
536
|
if message and message.parts and isinstance(message.parts[0].root, TextPart):
|
|
537
537
|
error = message.parts[0].root.text
|
|
538
|
-
console.print(f"[red]Task {status}[/red]
|
|
538
|
+
console.print(f"\n:boom: [red][bold]Task {status.value}[/bold][/red]")
|
|
539
|
+
console.print(Markdown(error))
|
|
539
540
|
return
|
|
540
541
|
case Task(id=task_id), TaskStatusUpdateEvent(
|
|
541
542
|
status=TaskStatus(state=TaskState.auth_required, message=message)
|
{agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/base_driver.py
RENAMED
|
@@ -112,11 +112,7 @@ class BaseDriver(abc.ABC):
|
|
|
112
112
|
"hostNetwork": True,
|
|
113
113
|
"externalRegistries": {"public_github": str(Configuration().agent_registry)},
|
|
114
114
|
"encryptionKey": "Ovx8qImylfooq4-HNwOzKKDcXLZCB3c_m0JlB9eJBxc=",
|
|
115
|
-
"features": {
|
|
116
|
-
"selfRegistration": True,
|
|
117
|
-
"generateConversationTitle": False, # TODO: enable when UI implementation is ready
|
|
118
|
-
"uiLocalSetup": True,
|
|
119
|
-
},
|
|
115
|
+
"features": {"uiLocalSetup": True},
|
|
120
116
|
"providerBuilds": {"enabled": True},
|
|
121
117
|
"localDockerRegistry": {"enabled": True},
|
|
122
118
|
"auth": {"enabled": False},
|
|
@@ -5,6 +5,7 @@ import asyncio
|
|
|
5
5
|
import logging
|
|
6
6
|
import sys
|
|
7
7
|
import typing
|
|
8
|
+
import uuid
|
|
8
9
|
import webbrowser
|
|
9
10
|
from urllib.parse import urlencode
|
|
10
11
|
|
|
@@ -68,6 +69,10 @@ async def _wait_for_auth_code(port: int = 9001) -> str:
|
|
|
68
69
|
return code
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
def get_unique_app_name() -> str:
|
|
73
|
+
return f"Agent Stack CLI {uuid.uuid4()}"
|
|
74
|
+
|
|
75
|
+
|
|
71
76
|
@app.command("login | change | select | default | switch")
|
|
72
77
|
async def server_login(server: typing.Annotated[str | None, typer.Argument()] = None):
|
|
73
78
|
"""Login to a server or switch between logged in servers."""
|
|
@@ -137,6 +142,7 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
137
142
|
|
|
138
143
|
client_id = config.client_id
|
|
139
144
|
client_secret = config.client_secret
|
|
145
|
+
registration_token = None
|
|
140
146
|
|
|
141
147
|
if auth_servers:
|
|
142
148
|
if len(auth_servers) == 1:
|
|
@@ -161,17 +167,35 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
161
167
|
registration_endpoint = oidc["registration_endpoint"]
|
|
162
168
|
if not client_id and registration_endpoint:
|
|
163
169
|
async with httpx.AsyncClient() as client:
|
|
170
|
+
resp = None
|
|
164
171
|
try:
|
|
172
|
+
app_name = get_unique_app_name()
|
|
165
173
|
resp = await client.post(
|
|
166
174
|
registration_endpoint,
|
|
167
|
-
json={
|
|
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
|
+
},
|
|
168
182
|
)
|
|
169
183
|
resp.raise_for_status()
|
|
170
184
|
data = resp.json()
|
|
171
185
|
client_id = data["client_id"]
|
|
172
186
|
client_secret = data["client_secret"]
|
|
173
|
-
|
|
174
|
-
|
|
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}")
|
|
175
199
|
|
|
176
200
|
if not client_id:
|
|
177
201
|
client_id = await inquirer.text( # type: ignore
|
|
@@ -208,6 +232,7 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
208
232
|
|
|
209
233
|
code = await _wait_for_auth_code()
|
|
210
234
|
async with httpx.AsyncClient() as client:
|
|
235
|
+
token_resp = None
|
|
211
236
|
try:
|
|
212
237
|
token_resp = await client.post(
|
|
213
238
|
oidc["token_endpoint"],
|
|
@@ -223,6 +248,15 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
223
248
|
token_resp.raise_for_status()
|
|
224
249
|
token = token_resp.json()
|
|
225
250
|
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
|
+
|
|
226
260
|
raise RuntimeError(f"Token request failed: {e}") from e
|
|
227
261
|
|
|
228
262
|
if not token:
|
|
@@ -234,6 +268,7 @@ async def server_login(server: typing.Annotated[str | None, typer.Argument()] =
|
|
|
234
268
|
client_id=client_id,
|
|
235
269
|
client_secret=client_secret,
|
|
236
270
|
token=token,
|
|
271
|
+
registration_token=registration_token,
|
|
237
272
|
)
|
|
238
273
|
|
|
239
274
|
config.auth_manager.active_server = server
|
|
@@ -248,7 +283,7 @@ async def server_logout(
|
|
|
248
283
|
typer.Option(),
|
|
249
284
|
] = False,
|
|
250
285
|
):
|
|
251
|
-
config.auth_manager.clear_auth_token(all=all)
|
|
286
|
+
await config.auth_manager.clear_auth_token(all=all)
|
|
252
287
|
console.success("You have been logged out.")
|
|
253
288
|
|
|
254
289
|
|
|
@@ -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=self.auth_manager.load_auth_token(),
|
|
68
|
+
auth_token=await 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
|
|
@@ -1,126 +0,0 @@
|
|
|
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()
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/__init__.py
RENAMED
|
File without changes
|
{agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/istio.py
RENAMED
|
File without changes
|
{agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/lima_driver.py
RENAMED
|
File without changes
|
{agentstack_cli-0.4.2rc9 → agentstack_cli-0.4.3}/src/agentstack_cli/commands/platform/wsl_driver.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|