forgexa-cli 1.31.1__tar.gz → 1.31.2__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.31.1 → forgexa_cli-1.31.2}/PKG-INFO +1 -1
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/daemon.py +5 -1
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/main.py +14 -9
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/pyproject.toml +1 -1
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_auth_and_runtime_commands.py +28 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_expiry_warnings_and_revoke.py +18 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/README.md +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/setup.cfg +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.31.
|
|
2
|
+
__version__ = "1.31.2"
|
|
@@ -910,7 +910,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
910
910
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
911
911
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
912
912
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
913
|
-
DAEMON_VERSION = "1.31.
|
|
913
|
+
DAEMON_VERSION = "1.31.2"
|
|
914
914
|
|
|
915
915
|
|
|
916
916
|
def _detect_client_type() -> str:
|
|
@@ -7101,6 +7101,10 @@ class ServerConnection:
|
|
|
7101
7101
|
self.client = httpx.AsyncClient(
|
|
7102
7102
|
headers={"Authorization": f"Bearer {api_token}"} if api_token else {},
|
|
7103
7103
|
verify=_make_httpx_ssl_context(),
|
|
7104
|
+
# Never go through an ambient system/env proxy: server_url is always an
|
|
7105
|
+
# operator-specified Forgexa server, and macOS's proxy bypass list isn't
|
|
7106
|
+
# honored here, which otherwise turns loopback calls into 502s.
|
|
7107
|
+
trust_env=False,
|
|
7104
7108
|
)
|
|
7105
7109
|
self.heartbeat: HeartbeatService | None = None
|
|
7106
7110
|
self.poller: TaskPoller | None = None
|
|
@@ -611,7 +611,12 @@ def _revoke_server_session(refresh_token: str) -> bool:
|
|
|
611
611
|
|
|
612
612
|
url = f"{_api_url()}/api/v1/auth/logout"
|
|
613
613
|
try:
|
|
614
|
-
|
|
614
|
+
# trust_env=False on every Forgexa-server client: httpx picks up the
|
|
615
|
+
# macOS system proxy (scutil) via urllib getproxies() without honoring
|
|
616
|
+
# the OS bypass list, routing loopback server calls through the proxy
|
|
617
|
+
# into 502s (same root cause as the daemon fix, 2026-08-28). Public
|
|
618
|
+
# internet calls (e.g. the PyPI update check) keep default behavior.
|
|
619
|
+
with httpx.Client(timeout=10, trust_env=False) as client:
|
|
615
620
|
resp = client.post(url, json={"refresh_token": refresh_token})
|
|
616
621
|
resp.raise_for_status()
|
|
617
622
|
except Exception:
|
|
@@ -625,7 +630,7 @@ def _submit_refresh_token(refresh_token: str) -> bool:
|
|
|
625
630
|
|
|
626
631
|
url = f"{_api_url()}/api/v1/auth/refresh"
|
|
627
632
|
try:
|
|
628
|
-
with httpx.Client(timeout=15) as client:
|
|
633
|
+
with httpx.Client(timeout=15, trust_env=False) as client:
|
|
629
634
|
resp = client.post(url, json={"refresh_token": refresh_token})
|
|
630
635
|
resp.raise_for_status()
|
|
631
636
|
payload = resp.json()
|
|
@@ -1606,7 +1611,7 @@ def _request_json(
|
|
|
1606
1611
|
|
|
1607
1612
|
url = f"{_api_url()}/api/v1{path}"
|
|
1608
1613
|
try:
|
|
1609
|
-
with httpx.Client(headers=_headers(), timeout=timeout) as client:
|
|
1614
|
+
with httpx.Client(headers=_headers(), timeout=timeout, trust_env=False) as client:
|
|
1610
1615
|
resp = client.request(method, url, json=data)
|
|
1611
1616
|
# Let callers (e.g. login) inspect the live client — notably its
|
|
1612
1617
|
# cookie jar — before it is discarded.
|
|
@@ -1870,11 +1875,11 @@ def _get_runtimes_silent(include_all: bool = False) -> list[dict]:
|
|
|
1870
1875
|
path = "/runtimes" if include_all else "/runtimes/me"
|
|
1871
1876
|
url = f"{_api_url()}/api/v1{path}"
|
|
1872
1877
|
try:
|
|
1873
|
-
with httpx.Client(headers=_headers(), timeout=10) as client:
|
|
1878
|
+
with httpx.Client(headers=_headers(), timeout=10, trust_env=False) as client:
|
|
1874
1879
|
resp = client.get(url)
|
|
1875
1880
|
if resp.status_code == 401 and _refresh_access_token():
|
|
1876
1881
|
# Retry once with the new token
|
|
1877
|
-
with httpx.Client(headers=_headers(), timeout=10) as retry_client:
|
|
1882
|
+
with httpx.Client(headers=_headers(), timeout=10, trust_env=False) as retry_client:
|
|
1878
1883
|
resp = retry_client.get(url)
|
|
1879
1884
|
resp.raise_for_status()
|
|
1880
1885
|
result = resp.json() if resp.content else []
|
|
@@ -2392,10 +2397,10 @@ def _delete_authenticated(path: str) -> tuple[int, dict]:
|
|
|
2392
2397
|
|
|
2393
2398
|
url = f"{_api_url()}/api/v1{path}"
|
|
2394
2399
|
try:
|
|
2395
|
-
with httpx.Client(headers=_headers(), timeout=30) as client:
|
|
2400
|
+
with httpx.Client(headers=_headers(), timeout=30, trust_env=False) as client:
|
|
2396
2401
|
resp = client.delete(url)
|
|
2397
2402
|
if resp.status_code == 401 and _refresh_access_token():
|
|
2398
|
-
with httpx.Client(headers=_headers(), timeout=30) as retry_client:
|
|
2403
|
+
with httpx.Client(headers=_headers(), timeout=30, trust_env=False) as retry_client:
|
|
2399
2404
|
resp = retry_client.delete(url)
|
|
2400
2405
|
except httpx.RequestError as e:
|
|
2401
2406
|
print(f"Connection error: {e}\nServer: {_api_url()}", file=sys.stderr)
|
|
@@ -2453,10 +2458,10 @@ def _post_authenticated(path: str, data: dict | None) -> tuple[int, dict]:
|
|
|
2453
2458
|
|
|
2454
2459
|
url = f"{_api_url()}/api/v1{path}"
|
|
2455
2460
|
try:
|
|
2456
|
-
with httpx.Client(headers=_headers(), timeout=30) as client:
|
|
2461
|
+
with httpx.Client(headers=_headers(), timeout=30, trust_env=False) as client:
|
|
2457
2462
|
resp = client.post(url, json=data or {})
|
|
2458
2463
|
if resp.status_code == 401 and _refresh_access_token():
|
|
2459
|
-
with httpx.Client(headers=_headers(), timeout=30) as retry_client:
|
|
2464
|
+
with httpx.Client(headers=_headers(), timeout=30, trust_env=False) as retry_client:
|
|
2460
2465
|
resp = retry_client.post(url, json=data or {})
|
|
2461
2466
|
except httpx.RequestError as e:
|
|
2462
2467
|
print(f"Connection error: {e}\nServer: {_api_url()}", file=sys.stderr)
|
|
@@ -1527,3 +1527,31 @@ async def test_daemon_submit_refresh_network_error_without_new_pair(
|
|
|
1527
1527
|
assert creds[_ISSUER]["refresh_token"] == "refresh-old"
|
|
1528
1528
|
finally:
|
|
1529
1529
|
await conn.client.aclose()
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def test_server_api_clients_disable_ambient_proxy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
1533
|
+
"""Regression (2026-08-28, same root cause as the daemon 502 fix): every
|
|
1534
|
+
Forgexa-server httpx client in main.py must set trust_env=False — on macOS
|
|
1535
|
+
httpx otherwise picks up the scutil system proxy via urllib getproxies()
|
|
1536
|
+
without honoring the OS bypass list, and calls to a loopback server come
|
|
1537
|
+
back as proxy-generated 502s. Driven through the generic _request_json
|
|
1538
|
+
helper and the auth refresh path, with the server pointed at an unbound
|
|
1539
|
+
loopback port so no real traffic leaves the machine."""
|
|
1540
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
1541
|
+
monkeypatch.setattr(main, "_SERVER_URL_OVERRIDE", "http://127.0.0.1:9")
|
|
1542
|
+
|
|
1543
|
+
captured: list[dict] = []
|
|
1544
|
+
real_client = httpx.Client
|
|
1545
|
+
|
|
1546
|
+
def spy_client(*args, **kwargs):
|
|
1547
|
+
captured.append(kwargs)
|
|
1548
|
+
return real_client(*args, **kwargs)
|
|
1549
|
+
|
|
1550
|
+
monkeypatch.setattr(httpx, "Client", spy_client)
|
|
1551
|
+
|
|
1552
|
+
with contextlib.suppress(SystemExit):
|
|
1553
|
+
main._request_json("/runtimes/me") # connection error → sys.exit(3)
|
|
1554
|
+
assert main._submit_refresh_token("refresh-token") is False
|
|
1555
|
+
|
|
1556
|
+
assert captured, "expected at least one httpx.Client construction"
|
|
1557
|
+
assert all(kwargs.get("trust_env") is False for kwargs in captured)
|
|
@@ -384,3 +384,21 @@ def test_daemon_silent_when_credential_valid_beyond_7_days(
|
|
|
384
384
|
import asyncio
|
|
385
385
|
|
|
386
386
|
asyncio.run(conn.client.aclose())
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_server_connection_client_disables_ambient_proxy(monkeypatch: pytest.MonkeyPatch):
|
|
390
|
+
"""Same regression as backend test_runtimes: the daemon's server client must
|
|
391
|
+
never ride an ambient system/env proxy — on macOS a scutil system proxy is
|
|
392
|
+
picked up by httpx (trust_env=True) without honoring the bypass list, and
|
|
393
|
+
loopback registration calls come back as 502s until the daemon exits."""
|
|
394
|
+
captured_kwargs: dict = {}
|
|
395
|
+
real_async_client = daemon.httpx.AsyncClient
|
|
396
|
+
|
|
397
|
+
def capture_init(*args, **kwargs):
|
|
398
|
+
captured_kwargs.update(kwargs)
|
|
399
|
+
return real_async_client(*args, **kwargs)
|
|
400
|
+
|
|
401
|
+
monkeypatch.setattr(daemon.httpx, "AsyncClient", capture_init)
|
|
402
|
+
daemon.ServerConnection(_ISSUER, "frt_secret_value", "daemon-1")
|
|
403
|
+
|
|
404
|
+
assert captured_kwargs.get("trust_env") is False
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|