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.
Files changed (27) hide show
  1. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/daemon.py +5 -1
  4. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/main.py +14 -9
  5. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/pyproject.toml +1 -1
  7. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_auth_and_runtime_commands.py +28 -0
  8. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_expiry_warnings_and_revoke.py +18 -0
  9. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/README.md +0 -0
  10. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/_build_config.py +0 -0
  11. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/_local_bind.py +0 -0
  12. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/agent_core.py +0 -0
  13. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/autoupgrade.py +0 -0
  14. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli/py.typed +0 -0
  15. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/setup.cfg +0 -0
  21. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_autoupgrade.py +0 -0
  22. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_check_command.py +0 -0
  23. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_local_bind_commands.py +0 -0
  24. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_runtime_credentials.py +0 -0
  25. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_session_credentials.py +0 -0
  26. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_silent_install.py +0 -0
  27. {forgexa_cli-1.31.1 → forgexa_cli-1.31.2}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.31.1
3
+ Version: 1.31.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.31.1"
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.1"
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
- with httpx.Client(timeout=10) as client:
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.31.1
3
+ Version: 1.31.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.31.1"
3
+ version = "1.31.2"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
@@ -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