forgexa-cli 1.17.6__tar.gz → 1.18.0__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.
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/PKG-INFO +1 -1
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli/daemon.py +70 -12
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/pyproject.toml +1 -1
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/README.md +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/setup.cfg +0 -0
- {forgexa_cli-1.17.6 → forgexa_cli-1.18.0}/tests/test_auth_and_runtime_commands.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.18.0"
|
|
@@ -45,7 +45,7 @@ import time
|
|
|
45
45
|
from dataclasses import dataclass, field
|
|
46
46
|
from datetime import datetime, timezone
|
|
47
47
|
from pathlib import Path
|
|
48
|
-
from typing import Any
|
|
48
|
+
from typing import Any, Awaitable, Callable
|
|
49
49
|
from urllib.parse import urlparse
|
|
50
50
|
from uuid import UUID
|
|
51
51
|
|
|
@@ -523,7 +523,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
523
523
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
524
524
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
525
525
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
526
|
-
DAEMON_VERSION = "1.
|
|
526
|
+
DAEMON_VERSION = "1.18.0"
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
def _detect_client_type() -> str:
|
|
@@ -6303,11 +6303,13 @@ class ServerConnection:
|
|
|
6303
6303
|
api_token: str,
|
|
6304
6304
|
daemon_id: str,
|
|
6305
6305
|
hardware_id: str | None = None,
|
|
6306
|
+
local_dev_token_provider: Callable[[], Awaitable[str]] | None = None,
|
|
6306
6307
|
):
|
|
6307
6308
|
self.server_url = server_url.rstrip("/")
|
|
6308
6309
|
self.api_token = api_token
|
|
6309
6310
|
self.daemon_id = daemon_id
|
|
6310
6311
|
self.hardware_id = hardware_id
|
|
6312
|
+
self._local_dev_token_provider = local_dev_token_provider
|
|
6311
6313
|
self.runtime_id: str | None = None
|
|
6312
6314
|
self.client = httpx.AsyncClient(
|
|
6313
6315
|
headers={"Authorization": f"Bearer {api_token}"} if api_token else {},
|
|
@@ -6322,7 +6324,12 @@ class ServerConnection:
|
|
|
6322
6324
|
# Short label for logging
|
|
6323
6325
|
from urllib.parse import urlparse
|
|
6324
6326
|
parsed = urlparse(server_url)
|
|
6327
|
+
hostname = (parsed.hostname or "").lower()
|
|
6325
6328
|
self.label = parsed.hostname or server_url
|
|
6329
|
+
self._allow_local_dev_token_fallback = (
|
|
6330
|
+
local_dev_token_provider is not None
|
|
6331
|
+
and hostname in {"localhost", "127.0.0.1", "::1"}
|
|
6332
|
+
)
|
|
6326
6333
|
|
|
6327
6334
|
def _apply_api_token(self, token: str) -> bool:
|
|
6328
6335
|
token = str(token or "").strip()
|
|
@@ -6339,6 +6346,22 @@ class ServerConnection:
|
|
|
6339
6346
|
except OSError:
|
|
6340
6347
|
return ""
|
|
6341
6348
|
|
|
6349
|
+
async def _apply_local_dev_token_fallback(self) -> bool:
|
|
6350
|
+
if not self._allow_local_dev_token_fallback or self._local_dev_token_provider is None:
|
|
6351
|
+
return False
|
|
6352
|
+
|
|
6353
|
+
try:
|
|
6354
|
+
token = str(await self._local_dev_token_provider() or "").strip()
|
|
6355
|
+
except Exception as exc:
|
|
6356
|
+
logger.debug("[%s] Local-dev token fallback failed: %s", self.label, exc)
|
|
6357
|
+
return False
|
|
6358
|
+
|
|
6359
|
+
if not self._apply_api_token(token):
|
|
6360
|
+
return False
|
|
6361
|
+
|
|
6362
|
+
logger.info("[%s] Falling back to a local-dev JWT for localhost registration", self.label)
|
|
6363
|
+
return True
|
|
6364
|
+
|
|
6342
6365
|
async def refresh_access_token(self) -> bool:
|
|
6343
6366
|
"""Refresh the daemon's user session token.
|
|
6344
6367
|
|
|
@@ -6456,8 +6479,11 @@ class ServerConnection:
|
|
|
6456
6479
|
|
|
6457
6480
|
try:
|
|
6458
6481
|
resp = await _register_once()
|
|
6459
|
-
if resp.status_code == 401
|
|
6460
|
-
|
|
6482
|
+
if resp.status_code == 401:
|
|
6483
|
+
if await self.refresh_access_token():
|
|
6484
|
+
resp = await _register_once()
|
|
6485
|
+
elif await self._apply_local_dev_token_fallback():
|
|
6486
|
+
resp = await _register_once()
|
|
6461
6487
|
resp.raise_for_status()
|
|
6462
6488
|
data = resp.json()
|
|
6463
6489
|
self.runtime_id = data["runtime_id"]
|
|
@@ -6567,6 +6593,7 @@ class RuntimeDaemon:
|
|
|
6567
6593
|
# prevents duplicate registrations when the hostname changes.
|
|
6568
6594
|
self.daemon_id = settings.DAEMON_ID or self.hardware_id or platform.node()
|
|
6569
6595
|
self.server_urls = settings.get_daemon_server_urls()
|
|
6596
|
+
self._has_explicit_api_token = bool(str(settings.DAEMON_API_TOKEN or "").strip())
|
|
6570
6597
|
self.api_token = settings.DAEMON_API_TOKEN
|
|
6571
6598
|
# If no explicit token, try user JWT from ~/.forgexa/token (written by `forgexa login`)
|
|
6572
6599
|
if not self.api_token:
|
|
@@ -6669,25 +6696,46 @@ class RuntimeDaemon:
|
|
|
6669
6696
|
async def _mint_local_dev_token() -> str:
|
|
6670
6697
|
"""Mint a JWT for local development when no explicit token is configured.
|
|
6671
6698
|
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6699
|
+
Queries the first available user and creates a long-lived access token so
|
|
6700
|
+
the runtime gets proper owner_id + organization_id assignment.
|
|
6701
|
+
|
|
6702
|
+
Avoid importing app.core.security here: some local host-Python setups used
|
|
6703
|
+
by `make dev` / `make restart` do not have password-hashing extras like
|
|
6704
|
+
bcrypt installed, but they can still mint JWTs for localhost development.
|
|
6675
6705
|
"""
|
|
6676
6706
|
try:
|
|
6677
|
-
from app.core.security import create_access_token
|
|
6678
6707
|
from app.database import engine
|
|
6679
6708
|
from datetime import timedelta
|
|
6680
|
-
from sqlalchemy import
|
|
6709
|
+
from sqlalchemy import case, select
|
|
6710
|
+
from app.models.user import User
|
|
6711
|
+
|
|
6712
|
+
try:
|
|
6713
|
+
import jwt as jwt_module
|
|
6714
|
+
except Exception:
|
|
6715
|
+
from jose import jwt as jwt_module
|
|
6681
6716
|
|
|
6682
6717
|
async with engine.connect() as conn:
|
|
6683
6718
|
row = await conn.execute(
|
|
6684
|
-
|
|
6719
|
+
select(User.id)
|
|
6720
|
+
.order_by(
|
|
6721
|
+
case((User.status == "active", 0), else_=1),
|
|
6722
|
+
User.created_at,
|
|
6723
|
+
)
|
|
6724
|
+
.limit(1)
|
|
6685
6725
|
)
|
|
6686
6726
|
r = row.first()
|
|
6687
6727
|
uid = str(r[0]) if r else None
|
|
6688
6728
|
|
|
6689
6729
|
if uid:
|
|
6690
|
-
token =
|
|
6730
|
+
token = jwt_module.encode(
|
|
6731
|
+
{
|
|
6732
|
+
"sub": uid,
|
|
6733
|
+
"type": "access",
|
|
6734
|
+
"exp": datetime.now(timezone.utc) + timedelta(days=30),
|
|
6735
|
+
},
|
|
6736
|
+
settings.SECRET_KEY,
|
|
6737
|
+
algorithm="HS256",
|
|
6738
|
+
)
|
|
6691
6739
|
logger.info("Minted local-dev JWT for user %s (no DAEMON_API_TOKEN configured)", uid)
|
|
6692
6740
|
return token
|
|
6693
6741
|
logger.warning("No active users in database; cannot mint local-dev token")
|
|
@@ -6928,9 +6976,19 @@ class RuntimeDaemon:
|
|
|
6928
6976
|
for url in self.server_urls:
|
|
6929
6977
|
if any(c.server_url == url.rstrip("/") for c in self.connections):
|
|
6930
6978
|
continue # Already connected to this server
|
|
6931
|
-
conn = ServerConnection(
|
|
6979
|
+
conn = ServerConnection(
|
|
6980
|
+
url,
|
|
6981
|
+
self.api_token,
|
|
6982
|
+
self.daemon_id,
|
|
6983
|
+
self.hardware_id,
|
|
6984
|
+
local_dev_token_provider=(
|
|
6985
|
+
self._mint_local_dev_token
|
|
6986
|
+
if not self._has_explicit_api_token else None
|
|
6987
|
+
),
|
|
6988
|
+
)
|
|
6932
6989
|
try:
|
|
6933
6990
|
await conn.register(self.agents, self.max_concurrent)
|
|
6991
|
+
self.api_token = conn.api_token
|
|
6934
6992
|
conn.start_services(self.heartbeat_interval, self.poll_interval, self.agents)
|
|
6935
6993
|
await conn.start_heartbeat()
|
|
6936
6994
|
self.connections.append(conn)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|