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