forgexa-cli 1.57.6__tar.gz → 1.57.7__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.57.6 → forgexa_cli-1.57.7}/PKG-INFO +1 -1
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/daemon.py +128 -21
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/pyproject.toml +1 -1
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/README.md +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/setup.cfg +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.57.
|
|
2
|
+
__version__ = "1.57.7"
|
|
@@ -1150,7 +1150,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1150
1150
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1151
1151
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1152
1152
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1153
|
-
DAEMON_VERSION = "1.57.
|
|
1153
|
+
DAEMON_VERSION = "1.57.7"
|
|
1154
1154
|
|
|
1155
1155
|
|
|
1156
1156
|
def _detect_client_type() -> str:
|
|
@@ -5507,6 +5507,10 @@ class _TaskAttachmentDownloadError(RuntimeError):
|
|
|
5507
5507
|
"""Raised when a required task attachment cannot be staged safely."""
|
|
5508
5508
|
|
|
5509
5509
|
|
|
5510
|
+
class _TaskAttachmentUnavailableError(_TaskAttachmentDownloadError):
|
|
5511
|
+
"""Raised when the attachment source is unavailable but the task can proceed."""
|
|
5512
|
+
|
|
5513
|
+
|
|
5510
5514
|
def _workspace_has_recent_activity(
|
|
5511
5515
|
workspace_path: "Path", since_monotonic: float, max_depth: int = 4
|
|
5512
5516
|
) -> bool:
|
|
@@ -5627,6 +5631,8 @@ class ProcessManager:
|
|
|
5627
5631
|
"authentication required",
|
|
5628
5632
|
"authenticate with the github",
|
|
5629
5633
|
"gh auth login",
|
|
5634
|
+
"your github token may be invalid",
|
|
5635
|
+
"rejected the cli github credential",
|
|
5630
5636
|
]
|
|
5631
5637
|
|
|
5632
5638
|
def __init__(self):
|
|
@@ -7640,6 +7646,59 @@ class ProcessManager:
|
|
|
7640
7646
|
return retry_result
|
|
7641
7647
|
_auth_state["persist"] = False
|
|
7642
7648
|
|
|
7649
|
+
auth_detail = " ".join(
|
|
7650
|
+
[*structured_errors, stderr]
|
|
7651
|
+
).lower()
|
|
7652
|
+
token_auth_error = (
|
|
7653
|
+
"your github token may be invalid" in auth_detail
|
|
7654
|
+
or "rejected the cli github credential" in auth_detail
|
|
7655
|
+
)
|
|
7656
|
+
if token_auth_error and not (copilot_exit == 0 and returncode == 0):
|
|
7657
|
+
_auth_state["persist"] = False
|
|
7658
|
+
if (
|
|
7659
|
+
token_auth_error
|
|
7660
|
+
and not (copilot_exit == 0 and returncode == 0)
|
|
7661
|
+
and not signals["has_meaningful_content"]
|
|
7662
|
+
and not self._copilot_called_any_tools(stdout)
|
|
7663
|
+
):
|
|
7664
|
+
recovery_home = (
|
|
7665
|
+
Path(_master_copilot_home)
|
|
7666
|
+
if auth_env_var and _master_copilot_home
|
|
7667
|
+
else agent_core.copilot_recovery_home(_isolated_copilot_home)
|
|
7668
|
+
)
|
|
7669
|
+
if (
|
|
7670
|
+
not _retry_without_token_env
|
|
7671
|
+
and recovery_home
|
|
7672
|
+
and (recovery_home / "config.json").is_file()
|
|
7673
|
+
):
|
|
7674
|
+
restored = await asyncio.to_thread(
|
|
7675
|
+
agent_core.replace_copilot_auth_state,
|
|
7676
|
+
recovery_home,
|
|
7677
|
+
_isolated_copilot_home,
|
|
7678
|
+
)
|
|
7679
|
+
can_retry_with_login = bool(auth_env_var or restored)
|
|
7680
|
+
else:
|
|
7681
|
+
can_retry_with_login = False
|
|
7682
|
+
if can_retry_with_login:
|
|
7683
|
+
logger.warning(
|
|
7684
|
+
"Copilot token authentication failed before work for task %s; "
|
|
7685
|
+
"retrying once with stored login and token environment variables removed",
|
|
7686
|
+
task_id,
|
|
7687
|
+
)
|
|
7688
|
+
retry_result = await self._run_copilot(
|
|
7689
|
+
agent, prompt, cwd, timeout, task_id, on_chunk,
|
|
7690
|
+
stream_stderr=stream_stderr,
|
|
7691
|
+
disable_builtin_mcps=disable_builtin_mcps,
|
|
7692
|
+
_retry_without_effort=_retry_without_effort,
|
|
7693
|
+
_retry_without_token_env=True,
|
|
7694
|
+
_isolated_copilot_home=_isolated_copilot_home,
|
|
7695
|
+
_auth_state=_auth_state,
|
|
7696
|
+
)
|
|
7697
|
+
_auth_state["persist"] = retry_result.status == "success"
|
|
7698
|
+
retry_result.metrics["copilot_auth_recovery"] = True
|
|
7699
|
+
return retry_result
|
|
7700
|
+
_auth_state["persist"] = False
|
|
7701
|
+
|
|
7643
7702
|
if copilot_exit == 0 and returncode == 0:
|
|
7644
7703
|
return TaskResult(
|
|
7645
7704
|
status="success",
|
|
@@ -7737,6 +7796,12 @@ class ProcessManager:
|
|
|
7737
7796
|
failure_error = f"Copilot exited with code {effective_rc}: {stderr[-500:].strip()}"
|
|
7738
7797
|
else:
|
|
7739
7798
|
failure_error = f"Copilot exited with code {effective_rc}"
|
|
7799
|
+
if token_auth_error:
|
|
7800
|
+
auth_source = auth_env_var or "stored Copilot login"
|
|
7801
|
+
failure_error = (
|
|
7802
|
+
f"Copilot authentication failed using {auth_source}. "
|
|
7803
|
+
f"{failure_error}"
|
|
7804
|
+
)
|
|
7740
7805
|
logger.error(
|
|
7741
7806
|
"Copilot task %s failed (process_rc=%s, result_exit=%s): %s",
|
|
7742
7807
|
task_id,
|
|
@@ -10574,9 +10639,22 @@ class RuntimeDaemon:
|
|
|
10574
10639
|
timeout=httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0),
|
|
10575
10640
|
) as response:
|
|
10576
10641
|
if not 200 <= response.status_code < 300:
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10642
|
+
error = f"Attachment '{attachment_name}' download failed with HTTP {response.status_code}"
|
|
10643
|
+
if response.status_code in {409, 502}:
|
|
10644
|
+
await response.aread()
|
|
10645
|
+
try:
|
|
10646
|
+
detail = response.json()
|
|
10647
|
+
except ValueError:
|
|
10648
|
+
detail = None
|
|
10649
|
+
if isinstance(detail, dict) and detail.get("detail") == "Task attachment integrity check failed":
|
|
10650
|
+
raise _TaskAttachmentDownloadError(error)
|
|
10651
|
+
if response.status_code == 409 and isinstance(detail, dict) and detail.get("detail") == "Task attachment source no longer matches stored checksum":
|
|
10652
|
+
raise _TaskAttachmentUnavailableError(
|
|
10653
|
+
f"Attachment '{attachment_name}' is unavailable: {detail['detail']}"
|
|
10654
|
+
)
|
|
10655
|
+
if response.status_code in {404, 502, 503, 504}:
|
|
10656
|
+
raise _TaskAttachmentUnavailableError(error)
|
|
10657
|
+
raise _TaskAttachmentDownloadError(error)
|
|
10580
10658
|
content_length = response.headers.get("content-length")
|
|
10581
10659
|
if content_length:
|
|
10582
10660
|
try:
|
|
@@ -10610,6 +10688,10 @@ class RuntimeDaemon:
|
|
|
10610
10688
|
temporary_path.replace(destination)
|
|
10611
10689
|
except _TaskAttachmentDownloadError:
|
|
10612
10690
|
raise
|
|
10691
|
+
except httpx.RequestError as exc:
|
|
10692
|
+
raise _TaskAttachmentUnavailableError(
|
|
10693
|
+
f"Attachment '{attachment_name}' download failed ({type(exc).__name__})"
|
|
10694
|
+
) from exc
|
|
10613
10695
|
except httpx.HTTPError as exc:
|
|
10614
10696
|
raise _TaskAttachmentDownloadError(
|
|
10615
10697
|
f"Attachment '{attachment_name}' download failed ({type(exc).__name__})"
|
|
@@ -10625,7 +10707,7 @@ class RuntimeDaemon:
|
|
|
10625
10707
|
timeout=timeout_seconds,
|
|
10626
10708
|
)
|
|
10627
10709
|
except asyncio.TimeoutError as exc:
|
|
10628
|
-
raise
|
|
10710
|
+
raise _TaskAttachmentUnavailableError(
|
|
10629
10711
|
f"Attachment '{attachment_name}' download timed out after "
|
|
10630
10712
|
f"{timeout_seconds:.0f}s"
|
|
10631
10713
|
) from exc
|
|
@@ -10648,6 +10730,7 @@ class RuntimeDaemon:
|
|
|
10648
10730
|
|
|
10649
10731
|
staging_dir = Path(tempfile.mkdtemp(prefix=f"forgexa-attachments-{task.task_id[:8]}-"))
|
|
10650
10732
|
staged: list[tuple[str, list[str], str]] = []
|
|
10733
|
+
unavailable: list[tuple[str, list[str], str]] = []
|
|
10651
10734
|
deadline = time.monotonic() + self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS
|
|
10652
10735
|
remaining_bytes = self._TASK_ATTACHMENT_MAX_TOTAL_BYTES
|
|
10653
10736
|
try:
|
|
@@ -10676,12 +10759,6 @@ class RuntimeDaemon:
|
|
|
10676
10759
|
raise _TaskAttachmentDownloadError(
|
|
10677
10760
|
f"Attachment '{file_name}' cannot be staged securely because it has no artifact ID"
|
|
10678
10761
|
)
|
|
10679
|
-
remaining_seconds = deadline - time.monotonic()
|
|
10680
|
-
if remaining_seconds <= 0:
|
|
10681
|
-
raise _TaskAttachmentDownloadError(
|
|
10682
|
-
f"Task attachment staging timed out after "
|
|
10683
|
-
f"{self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS}s"
|
|
10684
|
-
)
|
|
10685
10762
|
if not conn.runtime_id:
|
|
10686
10763
|
raise _TaskAttachmentDownloadError(
|
|
10687
10764
|
f"Attachment '{file_name}' cannot be staged because this runtime is not registered"
|
|
@@ -10690,16 +10767,27 @@ class RuntimeDaemon:
|
|
|
10690
10767
|
f"{conn.server_url.rstrip('/')}/api/v1/runtimes/{conn.runtime_id}/"
|
|
10691
10768
|
f"tasks/{task.task_id}/attachments/{artifact_id}"
|
|
10692
10769
|
)
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10770
|
+
try:
|
|
10771
|
+
remaining_seconds = deadline - time.monotonic()
|
|
10772
|
+
if remaining_seconds <= 0:
|
|
10773
|
+
raise _TaskAttachmentUnavailableError(
|
|
10774
|
+
f"Task attachment staging timed out after "
|
|
10775
|
+
f"{self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS}s"
|
|
10776
|
+
)
|
|
10777
|
+
await self._download_task_attachment(
|
|
10778
|
+
conn.client,
|
|
10779
|
+
source_url,
|
|
10780
|
+
destination,
|
|
10781
|
+
attachment_name=file_name,
|
|
10782
|
+
expected_size=expected_size or None,
|
|
10783
|
+
expected_checksum=str(attachment.get("checksum") or ""),
|
|
10784
|
+
max_bytes=min(self._TASK_ATTACHMENT_MAX_BYTES, remaining_bytes),
|
|
10785
|
+
timeout_seconds=remaining_seconds,
|
|
10786
|
+
)
|
|
10787
|
+
except _TaskAttachmentUnavailableError as exc:
|
|
10788
|
+
logger.warning("Task %s attachment unavailable: %s", task.task_id, exc)
|
|
10789
|
+
unavailable.append((file_name, download_url_aliases, str(exc)))
|
|
10790
|
+
continue
|
|
10703
10791
|
remaining_bytes -= destination.stat().st_size
|
|
10704
10792
|
staged.append((file_name, download_url_aliases, str(destination)))
|
|
10705
10793
|
except Exception:
|
|
@@ -10707,10 +10795,29 @@ class RuntimeDaemon:
|
|
|
10707
10795
|
raise
|
|
10708
10796
|
|
|
10709
10797
|
task.input_prompt = self._rewrite_staged_attachment_references(task.input_prompt, staged)
|
|
10798
|
+
if unavailable:
|
|
10799
|
+
notices = []
|
|
10800
|
+
for file_name, download_urls, error in unavailable:
|
|
10801
|
+
notices.append(f"- `{file_name}`: {error}")
|
|
10802
|
+
for download_url in download_urls:
|
|
10803
|
+
command_pattern = r"`curl -L '" + re.escape(download_url) + r"' -o /tmp/[^`]+`"
|
|
10804
|
+
task.input_prompt = re.sub(command_pattern, "[attachment unavailable]", task.input_prompt)
|
|
10805
|
+
task.input_prompt = task.input_prompt.replace(f"Download: {download_url}", "Attachment unavailable")
|
|
10806
|
+
task.input_prompt = task.input_prompt.replace(download_url, "[attachment unavailable]")
|
|
10807
|
+
task.input_prompt = "\n".join([
|
|
10808
|
+
"## Unavailable Attachments",
|
|
10809
|
+
"These files could not be retrieved. Continue with available inputs and report any missing evidence; do not retry their download URLs.",
|
|
10810
|
+
*notices,
|
|
10811
|
+
"",
|
|
10812
|
+
task.input_prompt,
|
|
10813
|
+
])
|
|
10710
10814
|
input_data = dict(task.input_data or {})
|
|
10711
10815
|
input_data["prompt"] = task.input_prompt
|
|
10712
10816
|
task.input_data = input_data
|
|
10713
10817
|
logger.info("Prepared %d attachment(s) for task %s", len(staged), task.task_id)
|
|
10818
|
+
if not staged:
|
|
10819
|
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
|
10820
|
+
return None
|
|
10714
10821
|
return staging_dir
|
|
10715
10822
|
|
|
10716
10823
|
@staticmethod
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|