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.
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/PKG-INFO +1 -1
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/agent_core.py +134 -1
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/daemon.py +61 -4
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/pyproject.toml +1 -1
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/README.md +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/setup.cfg +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.42.1 → forgexa_cli-1.43.2}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "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 =
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|