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.
Files changed (29) hide show
  1. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/PKG-INFO +1 -1
  2. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/daemon.py +128 -21
  4. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/PKG-INFO +1 -1
  5. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/pyproject.toml +1 -1
  6. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/README.md +0 -0
  7. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/_build_config.py +0 -0
  8. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/_local_bind.py +0 -0
  9. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/agent_core.py +0 -0
  10. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/setup.cfg +0 -0
  21. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.57.6 → forgexa_cli-1.57.7}/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.57.6
3
+ Version: 1.57.7
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.57.6"
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.6"
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
- raise _TaskAttachmentDownloadError(
10578
- f"Attachment '{attachment_name}' download failed with HTTP {response.status_code}"
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 _TaskAttachmentDownloadError(
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
- await self._download_task_attachment(
10694
- conn.client,
10695
- source_url,
10696
- destination,
10697
- attachment_name=file_name,
10698
- expected_size=expected_size or None,
10699
- expected_checksum=str(attachment.get("checksum") or ""),
10700
- max_bytes=min(self._TASK_ATTACHMENT_MAX_BYTES, remaining_bytes),
10701
- timeout_seconds=remaining_seconds,
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.57.6
3
+ Version: 1.57.7
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.57.6"
3
+ version = "1.57.7"
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