forgexa-cli 1.42.1__tar.gz → 1.43.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.42.1 → forgexa_cli-1.43.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/agent_core.py +134 -1
  4. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/daemon.py +61 -4
  5. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/pyproject.toml +1 -1
  7. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/README.md +0 -0
  8. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  14. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  15. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  16. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/requires.txt +0 -0
  17. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  18. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/setup.cfg +0 -0
  19. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_auth_and_runtime_commands.py +0 -0
  20. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_autoupgrade.py +0 -0
  21. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_check_command.py +0 -0
  22. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  23. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_local_bind_commands.py +0 -0
  24. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_runtime_credentials.py +0 -0
  25. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_session_credentials.py +0 -0
  26. {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_silent_install.py +0 -0
  27. {forgexa_cli-1.42.1 → forgexa_cli-1.43.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.42.1
3
+ Version: 1.43.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.42.1"
2
+ __version__ = "1.43.2"
@@ -1769,6 +1769,45 @@ def copilot_base_env(env: dict[str, str], *, copilot_home: str | Path) -> dict[s
1769
1769
  return env
1770
1770
 
1771
1771
 
1772
+ COPILOT_TOKEN_ENV_VARS = ("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN")
1773
+
1774
+
1775
+ def copilot_interactive_home() -> Path:
1776
+ """Return the login home selected for interactive Copilot CLI use."""
1777
+ configured_home = os.environ.get("COPILOT_HOME", "").strip()
1778
+ if configured_home:
1779
+ return Path(configured_home).expanduser()
1780
+ return Path.home() / ".copilot"
1781
+
1782
+
1783
+ def copilot_recovery_home(destination_home: str | Path) -> Path | None:
1784
+ """Return a populated login home distinct from the failed destination."""
1785
+ destination = Path(destination_home).resolve()
1786
+ candidates = (copilot_interactive_home(), Path.home() / ".copilot")
1787
+ seen: set[Path] = set()
1788
+ for candidate in candidates:
1789
+ resolved = candidate.resolve()
1790
+ if resolved in seen or resolved == destination:
1791
+ continue
1792
+ seen.add(resolved)
1793
+ if (resolved / "config.json").is_file():
1794
+ return resolved
1795
+ return None
1796
+
1797
+
1798
+ def is_copilot_model_discovery_auth_error(*messages: str | None) -> bool:
1799
+ """Return whether Copilot failed before work while resolving model access."""
1800
+ detail = " ".join(str(message or "").lower() for message in messages)
1801
+ return (
1802
+ "421" in detail
1803
+ and "misdirected request" in detail
1804
+ and (
1805
+ "retrieve the list of available models" in detail
1806
+ or "failed to load models" in detail
1807
+ )
1808
+ )
1809
+
1810
+
1772
1811
  def copilot_auth_state_signature(copilot_home: str | Path) -> str | None:
1773
1812
  """Return a content signature for Copilot's persisted authentication state."""
1774
1813
  config_file = Path(copilot_home) / "config.json"
@@ -1786,6 +1825,65 @@ def _copilot_auth_lock_path(copilot_home: str | Path) -> Path:
1786
1825
  return home.with_name(f".{home.name}-auth-state.lock")
1787
1826
 
1788
1827
 
1828
+ def replace_copilot_auth_state(
1829
+ source_home: str | Path,
1830
+ destination_home: str | Path,
1831
+ expected_destination_signature: str | None = None,
1832
+ ) -> bool:
1833
+ """Replace an isolated Copilot auth file from a known login home."""
1834
+ source = Path(source_home) / "config.json"
1835
+ destination_dir = Path(destination_home)
1836
+ destination = destination_dir / source.name
1837
+ if not source.is_file():
1838
+ return False
1839
+ lock_file = None
1840
+ temp_name: str | None = None
1841
+ try:
1842
+ source_signature = copilot_auth_state_signature(source_home)
1843
+ if source_signature is None:
1844
+ return False
1845
+ destination_dir.mkdir(parents=True, exist_ok=True)
1846
+ lock_file = acquire_file_lock(
1847
+ _copilot_auth_lock_path(destination_dir),
1848
+ blocking=True,
1849
+ )
1850
+ if lock_file is None:
1851
+ logger.warning("Could not lock Copilot authentication state for restore")
1852
+ return False
1853
+ destination_signature = copilot_auth_state_signature(destination_home)
1854
+ if (
1855
+ expected_destination_signature is not None
1856
+ and destination_signature != expected_destination_signature
1857
+ ):
1858
+ return False
1859
+ if destination_signature == source_signature:
1860
+ return False
1861
+ temp_fd, temp_name = tempfile.mkstemp(prefix=".config.json.", dir=destination_dir)
1862
+ os.close(temp_fd)
1863
+ shutil.copy2(source, temp_name)
1864
+ os.chmod(temp_name, 0o600)
1865
+ for attempt in range(3):
1866
+ try:
1867
+ os.replace(temp_name, destination)
1868
+ temp_name = None
1869
+ break
1870
+ except PermissionError:
1871
+ if sys.platform != "win32" or attempt == 2:
1872
+ raise
1873
+ time.sleep(0.05)
1874
+ return True
1875
+ except OSError:
1876
+ logger.warning("Could not restore interactive Copilot credentials", exc_info=True)
1877
+ return False
1878
+ finally:
1879
+ if temp_name is not None:
1880
+ try:
1881
+ Path(temp_name).unlink(missing_ok=True)
1882
+ except OSError:
1883
+ logger.debug("Could not remove temporary Copilot config", exc_info=True)
1884
+ release_file_lock(lock_file)
1885
+
1886
+
1789
1887
  def persist_copilot_auth_state(
1790
1888
  run_home: str | Path,
1791
1889
  persistent_home: str | Path,
@@ -1821,6 +1919,15 @@ def persist_copilot_auth_state(
1821
1919
  destination_signature = copilot_auth_state_signature(destination_dir)
1822
1920
  if destination_signature == source_signature:
1823
1921
  return
1922
+ if (
1923
+ baseline_signature is not None
1924
+ and destination_signature != baseline_signature
1925
+ ):
1926
+ logger.info(
1927
+ "Skipping stale Copilot authentication update because another "
1928
+ "run refreshed the persistent state first"
1929
+ )
1930
+ return
1824
1931
  if baseline_signature is None and destination.is_file():
1825
1932
  if destination.stat().st_mtime_ns >= source.stat().st_mtime_ns:
1826
1933
  return
@@ -1867,7 +1974,7 @@ def prepare_copilot_home() -> str:
1867
1974
  authentication state written by isolated runs is retained even when it
1868
1975
  has no counterpart in the interactive home.
1869
1976
  """
1870
- source_root = Path.home() / ".copilot"
1977
+ source_root = copilot_interactive_home()
1871
1978
  target_root = Path.home() / ".forgexa" / "copilot-home"
1872
1979
  lock_file = None
1873
1980
  try:
@@ -1879,6 +1986,8 @@ def prepare_copilot_home() -> str:
1879
1986
  if lock_file is None:
1880
1987
  logger.warning("Could not lock Copilot home for synchronization")
1881
1988
  return str(target_root)
1989
+ if source_root.resolve() == target_root.resolve():
1990
+ return str(target_root)
1882
1991
  if not source_root.exists():
1883
1992
  return str(target_root)
1884
1993
 
@@ -1920,6 +2029,20 @@ def prepare_copilot_home() -> str:
1920
2029
  migration_marker.touch(exist_ok=True)
1921
2030
  os.chmod(migration_marker, 0o600)
1922
2031
 
2032
+ source_auth_signature = copilot_auth_state_signature(source_root)
2033
+ source_signature_marker = target_root.parent / ".copilot-login-source.sha256"
2034
+ recorded_source_signature: str | None = None
2035
+ if source_signature_marker.is_file():
2036
+ try:
2037
+ recorded_source_signature = source_signature_marker.read_text().strip()
2038
+ except OSError:
2039
+ logger.debug("Could not read Copilot login source signature", exc_info=True)
2040
+ source_auth_changed = bool(
2041
+ source_auth_signature
2042
+ and recorded_source_signature is not None
2043
+ and source_auth_signature != recorded_source_signature
2044
+ )
2045
+ source_config_synced = False
1923
2046
  for child in source_root.iterdir():
1924
2047
  if child.name in {"logs", "session-state"}:
1925
2048
  continue
@@ -1937,11 +2060,15 @@ def prepare_copilot_home() -> str:
1937
2060
  elif child.is_file():
1938
2061
  if (
1939
2062
  child.name == "config.json"
2063
+ and not source_auth_changed
1940
2064
  and dest.is_file()
1941
2065
  and dest.stat().st_mtime_ns >= child.stat().st_mtime_ns
1942
2066
  ):
2067
+ source_config_synced = True
1943
2068
  continue
1944
2069
  shutil.copy2(child, dest)
2070
+ if child.name == "config.json":
2071
+ source_config_synced = True
1945
2072
  except OSError as exc:
1946
2073
  logger.debug(
1947
2074
  "Copilot home mirror skipped %s -> %s: %s",
@@ -1949,6 +2076,12 @@ def prepare_copilot_home() -> str:
1949
2076
  dest,
1950
2077
  exc,
1951
2078
  )
2079
+ if source_config_synced and source_auth_signature:
2080
+ try:
2081
+ source_signature_marker.write_text(source_auth_signature)
2082
+ os.chmod(source_signature_marker, 0o600)
2083
+ except OSError:
2084
+ logger.debug("Could not record Copilot login source signature", exc_info=True)
1952
2085
  except OSError:
1953
2086
  logger.warning("Could not prepare writable Copilot home", exc_info=True)
1954
2087
  finally:
@@ -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.42.1"
913
+ DAEMON_VERSION = "1.43.2"
914
914
 
915
915
 
916
916
  def _detect_client_type() -> str:
@@ -4781,7 +4781,8 @@ class ProcessManager:
4781
4781
  "error during sign-in",
4782
4782
  "websockettransporterror",
4783
4783
  "websocket receive failed",
4784
- "421 misdirected request",
4784
+ "misdirected request",
4785
+ "could not retrieve the list of available models",
4785
4786
  "failed to initialize mcp client",
4786
4787
  # "api error" removed: too broad — matches agent-generated code/output
4787
4788
  # discussing API errors. Real API transport errors are covered by the
@@ -5996,7 +5997,9 @@ class ProcessManager:
5996
5997
  *,
5997
5998
  disable_builtin_mcps: bool = False,
5998
5999
  _retry_without_effort: bool = False,
6000
+ _retry_without_token_env: bool = False,
5999
6001
  _isolated_copilot_home: str | None = None,
6002
+ _auth_state: dict[str, bool] | None = None,
6000
6003
  ) -> TaskResult:
6001
6004
  """Run GitHub Copilot CLI in non-interactive JSON-streaming mode.
6002
6005
 
@@ -6032,6 +6035,8 @@ class ProcessManager:
6032
6035
  # session-state persists from the prior attempt all todos are already
6033
6036
  # "done" and the agent exits immediately without producing any output.
6034
6037
  _is_outer_call = _isolated_copilot_home is None
6038
+ if _auth_state is None:
6039
+ _auth_state = {"persist": True}
6035
6040
  _master_copilot_home: str | None = None
6036
6041
  _copilot_auth_baseline: str | None = None
6037
6042
  if _is_outer_call:
@@ -6059,10 +6064,13 @@ class ProcessManager:
6059
6064
  os.environ.copy(),
6060
6065
  copilot_home=_isolated_copilot_home,
6061
6066
  )
6067
+ if _retry_without_token_env:
6068
+ for key in agent_core.COPILOT_TOKEN_ENV_VARS:
6069
+ env.pop(key, None)
6062
6070
  auth_env_var = next(
6063
6071
  (
6064
6072
  key
6065
- for key in ("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN")
6073
+ for key in agent_core.COPILOT_TOKEN_ENV_VARS
6066
6074
  if env.get(key)
6067
6075
  ),
6068
6076
  None,
@@ -6230,11 +6238,58 @@ class ProcessManager:
6230
6238
  on_chunk,
6231
6239
  disable_builtin_mcps=True,
6232
6240
  _retry_without_effort=_retry_without_effort,
6241
+ _retry_without_token_env=_retry_without_token_env,
6233
6242
  _isolated_copilot_home=_isolated_copilot_home,
6243
+ _auth_state=_auth_state,
6234
6244
  )
6235
6245
  retry_result.metrics["copilot_builtin_mcp_fallback"] = True
6236
6246
  return retry_result
6237
6247
 
6248
+ model_discovery_error = agent_core.is_copilot_model_discovery_auth_error(
6249
+ *structured_errors,
6250
+ stderr,
6251
+ )
6252
+ if model_discovery_error and not self._copilot_called_any_tools(stdout):
6253
+ isolated_config = Path(_isolated_copilot_home) / "config.json"
6254
+ recovery_home = agent_core.copilot_recovery_home(
6255
+ _isolated_copilot_home,
6256
+ )
6257
+ can_retry_native_auth = bool(
6258
+ isolated_config.is_file()
6259
+ or recovery_home is not None
6260
+ )
6261
+ if not _retry_without_token_env and can_retry_native_auth:
6262
+ restored_interactive_auth = bool(
6263
+ recovery_home
6264
+ and await asyncio.to_thread(
6265
+ agent_core.replace_copilot_auth_state,
6266
+ recovery_home,
6267
+ _isolated_copilot_home,
6268
+ )
6269
+ )
6270
+ logger.warning(
6271
+ "Copilot model discovery failed with HTTP 421 before work for "
6272
+ "task %s; retrying once with token environment variables removed%s",
6273
+ task_id,
6274
+ " and interactive login restored" if restored_interactive_auth else "",
6275
+ )
6276
+ retry_result = await self._run_copilot(
6277
+ agent,
6278
+ prompt,
6279
+ cwd,
6280
+ timeout,
6281
+ task_id,
6282
+ on_chunk,
6283
+ disable_builtin_mcps=disable_builtin_mcps,
6284
+ _retry_without_effort=_retry_without_effort,
6285
+ _retry_without_token_env=True,
6286
+ _isolated_copilot_home=_isolated_copilot_home,
6287
+ _auth_state=_auth_state,
6288
+ )
6289
+ retry_result.metrics["copilot_model_discovery_recovery"] = True
6290
+ return retry_result
6291
+ _auth_state["persist"] = False
6292
+
6238
6293
  if copilot_exit == 0 and returncode == 0:
6239
6294
  return TaskResult(
6240
6295
  status="success",
@@ -6290,7 +6345,9 @@ class ProcessManager:
6290
6345
  agent, prompt, cwd, timeout, task_id, on_chunk,
6291
6346
  disable_builtin_mcps=disable_builtin_mcps,
6292
6347
  _retry_without_effort=True,
6348
+ _retry_without_token_env=_retry_without_token_env,
6293
6349
  _isolated_copilot_home=_isolated_copilot_home,
6350
+ _auth_state=_auth_state,
6294
6351
  )
6295
6352
  # Exhausted retries — emit an actionable diagnostic hint
6296
6353
  if effective_rc == 1 and _no_tools and not copilot_specific_error:
@@ -6390,7 +6447,7 @@ class ProcessManager:
6390
6447
  finally:
6391
6448
  self.active_processes.pop(task_id, None)
6392
6449
  if _is_outer_call and _isolated_copilot_home:
6393
- if _master_copilot_home:
6450
+ if _master_copilot_home and _auth_state["persist"]:
6394
6451
  await asyncio.to_thread(
6395
6452
  agent_core.persist_copilot_auth_state,
6396
6453
  _isolated_copilot_home,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.42.1
3
+ Version: 1.43.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.42.1"
3
+ version = "1.43.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"
File without changes
File without changes