akashcli 4.5.2__tar.gz → 4.5.4__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 (32) hide show
  1. {akashcli-4.5.2 → akashcli-4.5.4}/PKG-INFO +2 -1
  2. akashcli-4.5.4/akashcli/_net.py +177 -0
  3. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/client.py +4 -0
  4. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/exec.py +19 -14
  5. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/relay.py +17 -29
  6. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/PKG-INFO +2 -1
  7. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/SOURCES.txt +1 -0
  8. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/requires.txt +1 -0
  9. {akashcli-4.5.2 → akashcli-4.5.4}/pyproject.toml +2 -1
  10. {akashcli-4.5.2 → akashcli-4.5.4}/README.md +0 -0
  11. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/__init__.py +0 -0
  12. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/auth.py +0 -0
  13. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/cli.py +0 -0
  14. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/config.py +0 -0
  15. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/crypto.py +0 -0
  16. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/env_manager.py +0 -0
  17. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/exceptions.py +0 -0
  18. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/integrity.py +0 -0
  19. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/logger.py +0 -0
  20. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/models.py +0 -0
  21. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/p2p.py +0 -0
  22. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/packages.py +0 -0
  23. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/restore.py +0 -0
  24. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/snapshot_crypto.py +0 -0
  25. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/storage.py +0 -0
  26. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/terminal.py +0 -0
  27. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/uploader.py +0 -0
  28. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli/utils.py +0 -0
  29. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/dependency_links.txt +0 -0
  30. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/entry_points.txt +0 -0
  31. {akashcli-4.5.2 → akashcli-4.5.4}/akashcli.egg-info/top_level.txt +0 -0
  32. {akashcli-4.5.2 → akashcli-4.5.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.5.2
3
+ Version: 4.5.4
4
4
  Summary: Akash CLI — secure remote shell relay, file vault, and device management
5
5
  License: MIT
6
6
  Keywords: akash,cli,relay,remote-shell,vault
@@ -12,6 +12,7 @@ Classifier: Topic :: Utilities
12
12
  Requires-Python: >=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  Requires-Dist: httpx
15
+ Requires-Dist: certifi
15
16
  Requires-Dist: typer
16
17
  Requires-Dist: cryptography
17
18
  Requires-Dist: pydantic
@@ -0,0 +1,177 @@
1
+ """
2
+ _net.py — Network helpers for akashcli.
3
+
4
+ On Android/Termux the system DNS daemon (netd) may cache stale IP records
5
+ for the server long after they have changed. curl uses Android's native
6
+ networking APIs and sees fresh records; Python's socket.getaddrinfo() goes
7
+ through netd and gets the old IP, causing all connections to time out.
8
+
9
+ This module provides:
10
+ • doh_resolve(host) — resolve a hostname via Google DoH using curl subprocess
11
+ • patch_dns(host) — globally patch socket.create_connection for one host
12
+ • ws_connect(url) — websockets.sync.client.connect() with DoH pre-resolution
13
+ • ssl_context() — SSLContext using certifi CA bundle
14
+
15
+ All functions are safe to call on non-Android systems (they simply do nothing
16
+ extra).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json as _json
22
+ import shutil as _sh
23
+ import socket as _socket
24
+ import ssl as _ssl
25
+ import subprocess as _sp
26
+ from typing import Optional
27
+ from urllib.parse import urlparse as _urlparse
28
+
29
+ # ── per-host DoH cache (lives for the process lifetime) ──────────────────────
30
+ _DOH_CACHE: dict[str, str] = {}
31
+
32
+
33
+ def doh_resolve(host: str) -> Optional[str]:
34
+ """
35
+ Resolve *host* via DNS-over-HTTPS (Google) using a curl subprocess.
36
+ Returns the first A-record IP string, or None on any failure.
37
+
38
+ Uses curl because it goes through Android's native networking stack and
39
+ therefore sees up-to-date DNS records, unlike Python's socket.getaddrinfo
40
+ which may hit Android's netd stale cache.
41
+ """
42
+ if host in _DOH_CACHE:
43
+ return _DOH_CACHE[host]
44
+
45
+ curl = _sh.which("curl")
46
+ if not curl:
47
+ return None
48
+ try:
49
+ r = _sp.run(
50
+ [curl, "-sk", "--max-time", "8",
51
+ f"https://dns.google/resolve?name={host}&type=A"],
52
+ capture_output=True, timeout=10,
53
+ )
54
+ data = _json.loads(r.stdout)
55
+ answers = [a["data"] for a in data.get("Answer", [])
56
+ if a.get("type") == 1] # type 1 == A record
57
+ if answers:
58
+ _DOH_CACHE[host] = answers[0]
59
+ return answers[0]
60
+ except Exception:
61
+ pass
62
+ return None
63
+
64
+
65
+ def ssl_context() -> _ssl.SSLContext:
66
+ """Return an SSLContext backed by certifi's CA bundle (if installed)."""
67
+ try:
68
+ import certifi as _cf
69
+ return _ssl.create_default_context(cafile=_cf.where())
70
+ except ImportError:
71
+ return _ssl.create_default_context()
72
+
73
+
74
+ def make_socket(host: str, port: int, timeout: int = 15) -> _socket.socket:
75
+ """
76
+ Open a TCP socket to host:port.
77
+ If DoH returns a different IP than what getaddrinfo would return, use the
78
+ DoH IP with the host as SNI (caller must wrap TLS themselves).
79
+ Falls back to normal getaddrinfo on any failure.
80
+ """
81
+ ip = doh_resolve(host)
82
+ if ip:
83
+ try:
84
+ return _socket.create_connection((ip, port), timeout=timeout)
85
+ except Exception:
86
+ pass
87
+ return _socket.create_connection((host, port), timeout=timeout)
88
+
89
+
90
+ def ws_connect(url: str, **kwargs):
91
+ """
92
+ websockets.sync.client.connect() with DoH-resolved IP.
93
+ Pass open_timeout / close_timeout etc. as keyword arguments as normal.
94
+ The special kwarg _sock_timeout (int, default 15) controls the TCP connect
95
+ timeout before the WebSocket handshake.
96
+ """
97
+ import websockets.sync.client as _wsc
98
+
99
+ parsed = _urlparse(url)
100
+ host = parsed.hostname or ""
101
+ port = parsed.port or (443 if parsed.scheme == "wss" else 80)
102
+ use_tls = parsed.scheme == "wss"
103
+
104
+ sock_timeout = kwargs.pop("_sock_timeout", 15)
105
+ sock = make_socket(host, port, timeout=sock_timeout)
106
+
107
+ if use_tls:
108
+ return _wsc.connect(url, sock=sock, ssl=ssl_context(), **kwargs)
109
+ return _wsc.connect(url, sock=sock, **kwargs)
110
+
111
+
112
+ def http_post(url: str, *, timeout: int = 10, **kwargs) -> dict:
113
+ """
114
+ POST *url* and return the parsed JSON response dict.
115
+ Uses curl subprocess (bypasses Android stale DNS) and falls back to
116
+ urllib with a DoH-patched socket on failure.
117
+ """
118
+ parsed = _urlparse(url)
119
+ host = parsed.hostname or ""
120
+ real_ip = doh_resolve(host)
121
+
122
+ # ── 1. curl subprocess ────────────────────────────────────────────────────
123
+ curl = _sh.which("curl")
124
+ if curl:
125
+ cmd = [curl, "-sk", "--max-time", str(timeout), "-X", "POST"]
126
+ if real_ip:
127
+ cmd += ["--resolve", f"{host}:443:{real_ip}"]
128
+ cmd.append(url)
129
+ try:
130
+ r = _sp.run(cmd, capture_output=True, timeout=timeout + 5)
131
+ if r.returncode == 0 and r.stdout:
132
+ return _json.loads(r.stdout)
133
+ except Exception:
134
+ pass
135
+
136
+ # ── 2. urllib with monkey-patched socket ──────────────────────────────────
137
+ import urllib.request as _ur
138
+ from urllib.request import HTTPSHandler as _HH
139
+
140
+ ctx = ssl_context()
141
+
142
+ if real_ip:
143
+ _orig = _socket.create_connection
144
+ def _patched(addr, *a, **kw):
145
+ h, p = addr
146
+ return _orig((real_ip, p) if h == host else addr, *a, **kw)
147
+ _socket.create_connection = _patched
148
+
149
+ try:
150
+ opener = _ur.build_opener(_HH(context=ctx))
151
+ req = _ur.Request(url, data=b"", method="POST")
152
+ with opener.open(req, timeout=timeout) as resp:
153
+ return _json.loads(resp.read())
154
+ finally:
155
+ if real_ip:
156
+ _socket.create_connection = _orig # type: ignore[possibly-undefined]
157
+
158
+
159
+ def patch_dns_globally(host: str) -> None:
160
+ """
161
+ Permanently patch socket.create_connection so that connections to *host*
162
+ use the DoH-resolved IP. Safe to call multiple times.
163
+ Intended to be called once at CLI startup for the configured server host.
164
+ """
165
+ ip = doh_resolve(host)
166
+ if not ip:
167
+ return
168
+
169
+ _orig = getattr(_socket, "_akash_orig_create", _socket.create_connection)
170
+ _socket._akash_orig_create = _orig # type: ignore[attr-defined]
171
+
172
+ def _patched(addr, *args, **kwargs):
173
+ h, p = addr
174
+ real = ip if h == host else None
175
+ return _orig((real or h, p), *args, **kwargs)
176
+
177
+ _socket.create_connection = _patched
@@ -4,16 +4,20 @@ from typing import List, Optional
4
4
  from .models import UploadResult, FileInfo, UserInfo
5
5
  from .exceptions import *
6
6
  from .config import ConfigManager
7
+ from ._net import patch_dns_globally as _patch_dns
7
8
 
8
9
  class AkashClient:
9
10
  def __init__(self, api_key: Optional[str] = None, api_url: Optional[str] = None):
10
11
  import os as _os
12
+ from urllib.parse import urlparse as _up
11
13
  config = ConfigManager.load()
12
14
  # Env vars override everything — useful when hardware-ID encryption differs across su sessions
13
15
  self.api_key = api_key or _os.getenv("AKASH_API_KEY") or config.get("api_key")
14
16
  self.base_url = (
15
17
  api_url or _os.getenv("AKASH_API_URL") or config.get("api_url", "https://jstore.2bd.net")
16
18
  ).rstrip("/")
19
+ # Patch socket DNS to bypass Android stale DNS cache
20
+ _patch_dns(_up(self.base_url).hostname or "")
17
21
 
18
22
  if not self.api_key:
19
23
  raise AuthenticationError("No API Key found. Run 'akash login' or set AKASH_API_KEY env var.")
@@ -48,6 +48,7 @@ from pathlib import Path
48
48
  from typing import Optional
49
49
 
50
50
  import httpx
51
+ from ._net import patch_dns_globally as _patch_dns, ws_connect as _ws_connect_net
51
52
 
52
53
  # ── Paths & constants ────────────────────────────────────────────────────────
53
54
 
@@ -241,6 +242,10 @@ def acquire_exec_token(api_key: str, base_url: str, controller: bool = False) ->
241
242
  connect / run / shell). Just authenticates as owner.
242
243
  controller=False — full challenge/sign/enroll flow (used by akash serve).
243
244
  """
245
+ # Patch socket DNS for this host — bypasses Android stale DNS cache
246
+ from urllib.parse import urlparse as _up
247
+ _patch_dns(_up(base_url).hostname or "")
248
+
244
249
  token = _load_cached_token(base_url)
245
250
  if token:
246
251
  return token
@@ -327,11 +332,11 @@ def kill_session(token: str, base_url: str, session_id: str) -> None:
327
332
 
328
333
  def run_command(token: str, base_url: str, command: str,
329
334
  env: Optional[dict] = None) -> int:
330
- import websockets.sync.client as _ws
335
+
331
336
  ws_url = _build_ws_url(base_url, token)
332
337
  exit_code = 1
333
338
  try:
334
- with _ws.connect(ws_url, open_timeout=15, close_timeout=10) as ws:
339
+ with _ws_connect_net(ws_url, open_timeout=15, close_timeout=10) as ws:
335
340
  ws.send(json.dumps({"type": "run", "command": command, "env": env or {}}))
336
341
  while True:
337
342
  try:
@@ -367,7 +372,7 @@ def run_shell(token: str, base_url: str) -> int:
367
372
  Commands run in the same shell process — cd, export, and variables persist.
368
373
  Output streams live with ANSI colours passed through.
369
374
  """
370
- import websockets.sync.client as _ws
375
+
371
376
 
372
377
  _load_shell_history()
373
378
 
@@ -557,7 +562,7 @@ def _run_tui(command: str, token: str, base_url: str,
557
562
  """
558
563
  import select as _sel
559
564
  import signal
560
- import websockets.sync.client as _ws
565
+
561
566
 
562
567
  ws_url = _build_ws_url(base_url, token)
563
568
  rows, cols = _get_terminal_size()
@@ -586,7 +591,7 @@ def _run_tui(command: str, token: str, base_url: str,
586
591
  exit_code = 1
587
592
 
588
593
  try:
589
- with _ws.connect(ws_url, open_timeout=15, close_timeout=10) as ws:
594
+ with _ws_connect_net(ws_url, open_timeout=15, close_timeout=10) as ws:
590
595
  # Send PTY handshake
591
596
  ws.send(json.dumps({
592
597
  "type": "pty_run",
@@ -702,7 +707,7 @@ def _exec_remote(command: str, token: str, base_url: str,
702
707
  first_output = True
703
708
 
704
709
  try:
705
- with _ws.connect(ws_url, open_timeout=15, close_timeout=15) as ws:
710
+ with _ws_connect_net(ws_url, open_timeout=15, close_timeout=15) as ws:
706
711
  ws.send(json.dumps({
707
712
  "type": "run",
708
713
  "command": command,
@@ -754,9 +759,9 @@ def _exec_remote(command: str, token: str, base_url: str,
754
759
  def _fetch_cwd(token: str, base_url: str, session_id: Optional[str]) -> str:
755
760
  """Ask the server for the current working directory."""
756
761
  try:
757
- import websockets.sync.client as _ws
762
+
758
763
  ws_url = _build_ws_url(base_url, token)
759
- with _ws.connect(ws_url, open_timeout=8, close_timeout=5) as ws:
764
+ with _ws_connect_net(ws_url, open_timeout=8, close_timeout=5) as ws:
760
765
  ws.send(json.dumps({
761
766
  "type": "run", "command": "pwd",
762
767
  "session_id": session_id, "track_cwd": True,
@@ -836,7 +841,7 @@ def _cmd_session(token: str, base_url: str, session_id: Optional[str],
836
841
 
837
842
  def _cmd_status(token: str, base_url: str, session_id: Optional[str]) -> int:
838
843
  # Run a compound command to fetch server stats
839
- import websockets.sync.client as _ws
844
+
840
845
  cmd = (
841
846
  r'printf "cwd=%s\npid=%s\nload=%s\nuptime=%s\nshell=%s\nuser=%s\nhostname=%s\n" '
842
847
  r'"$(pwd)" "$$" "$(cut -d" " -f1-3 /proc/loadavg 2>/dev/null || uptime | grep -oE "[0-9.]+, [0-9.]+, [0-9.]+")" '
@@ -846,7 +851,7 @@ def _cmd_status(token: str, base_url: str, session_id: Optional[str]) -> int:
846
851
  ws_url = _build_ws_url(base_url, token)
847
852
  output = b""
848
853
  try:
849
- with _ws.connect(ws_url, open_timeout=8, close_timeout=5) as ws:
854
+ with _ws_connect_net(ws_url, open_timeout=8, close_timeout=5) as ws:
850
855
  ws.send(json.dumps({"type": "run", "command": cmd, "session_id": session_id}))
851
856
  while True:
852
857
  raw = ws.recv()
@@ -883,12 +888,12 @@ def _cmd_status(token: str, base_url: str, session_id: Optional[str]) -> int:
883
888
 
884
889
 
885
890
  def _cmd_env(token: str, base_url: str, session_id: Optional[str]) -> int:
886
- import websockets.sync.client as _ws
891
+
887
892
  ws_url = _build_ws_url(base_url, token)
888
893
  output = b""
889
894
  ec = 0
890
895
  try:
891
- with _ws.connect(ws_url, open_timeout=8, close_timeout=5) as ws:
896
+ with _ws_connect_net(ws_url, open_timeout=8, close_timeout=5) as ws:
892
897
  ws.send(json.dumps({"type": "run", "command": "env | sort", "session_id": session_id}))
893
898
  while True:
894
899
  raw = ws.recv()
@@ -917,12 +922,12 @@ def _cmd_export(kv: str, token: str, base_url: str, session_id: Optional[str]) -
917
922
  if not kv or "=" not in kv:
918
923
  _print_err("Usage: :export KEY=VALUE")
919
924
  return 1
920
- import websockets.sync.client as _ws
925
+
921
926
  ws_url = _build_ws_url(base_url, token)
922
927
  cmd = f"export {kv}"
923
928
  ec = 0
924
929
  try:
925
- with _ws.connect(ws_url, open_timeout=8, close_timeout=5) as ws:
930
+ with _ws_connect_net(ws_url, open_timeout=8, close_timeout=5) as ws:
926
931
  ws.send(json.dumps({"type": "run", "command": cmd, "session_id": session_id}))
927
932
  while True:
928
933
  raw = ws.recv()
@@ -48,6 +48,16 @@ _SERVE_PID = _AKASH_DIR / "serve.pid"
48
48
  _SERVE_LOG = _AKASH_DIR / "serve.log"
49
49
 
50
50
 
51
+ # ── Network helpers (bypass Android stale DNS) ────────────────────────────────
52
+ from ._net import (
53
+ doh_resolve as _doh_resolve,
54
+ make_socket as _make_socket,
55
+ ssl_context as _ssl_context,
56
+ ws_connect as _ws_connect,
57
+ http_post as _http_post,
58
+ )
59
+
60
+
51
61
  # ── WS URL builder ────────────────────────────────────────────────────────────
52
62
 
53
63
  def _ws_base(http_url: str) -> str:
@@ -85,14 +95,12 @@ def serve(api_key: str, base_url: str, label: str, daemon: bool = False) -> None
85
95
  print(f" {_G}◉{_RS} akash serve {_DM}label={label}{_RS}")
86
96
  print(f" {_DM}connecting to relay…{_RS}")
87
97
 
88
- import websockets.sync.client as _ws
89
-
90
98
  while True: # reconnect loop
91
99
  try:
92
100
  token = acquire_exec_token(api_key, base_url)
93
101
  ws_url = _listen_url(base_url, token)
94
102
 
95
- with _ws.connect(ws_url, open_timeout=15, close_timeout=5) as ws:
103
+ with _ws_connect(ws_url, open_timeout=15, close_timeout=5) as ws:
96
104
  # Send listen handshake
97
105
  ws.send(json.dumps({"type": "listen", "label": label}))
98
106
  # Wait for server ack
@@ -295,13 +303,12 @@ def connect_run(api_key: str, base_url: str, command: str,
295
303
  Returns the remote exit code.
296
304
  """
297
305
  from .exec import acquire_exec_token
298
- import websockets.sync.client as _ws
299
306
 
300
307
  token = acquire_exec_token(api_key, base_url, controller=True)
301
308
  ws_url = _connect_url(base_url, token, target)
302
309
  exit_code = 1
303
310
 
304
- with _ws.connect(ws_url, open_timeout=15, close_timeout=10) as ws:
311
+ with _ws_connect(ws_url, open_timeout=15, close_timeout=10) as ws:
305
312
  # Wait for tunnel confirmation
306
313
  ack = json.loads(ws.recv())
307
314
  if ack.get("type") == "error":
@@ -345,7 +352,6 @@ def connect_shell(api_key: str, base_url: str,
345
352
  Returns the remote exit code.
346
353
  """
347
354
  from .exec import acquire_exec_token, _get_terminal_size
348
- import websockets.sync.client as _ws
349
355
  import select as _sel, signal
350
356
 
351
357
  token = acquire_exec_token(api_key, base_url, controller=True)
@@ -376,7 +382,7 @@ def connect_shell(api_key: str, base_url: str,
376
382
  exit_code = 1
377
383
 
378
384
  try:
379
- with _ws.connect(ws_url, open_timeout=15, close_timeout=10) as ws:
385
+ with _ws_connect(ws_url, open_timeout=15, close_timeout=10) as ws:
380
386
  # Wait for tunnel open
381
387
  ack = json.loads(ws.recv())
382
388
  if ack.get("type") == "error":
@@ -558,26 +564,9 @@ def guest_serve(base_url: str) -> None:
558
564
  Security: every incoming connection shows the guest IP + fingerprint
559
565
  and asks the host to approve before the shell is bridged.
560
566
  """
561
- import websockets.sync.client as _ws
562
-
563
- # 1. Get a pairing code — try httpx first, fall back to urllib (system certs)
564
- def _post_code(url: str) -> dict:
565
- try:
566
- import httpx as _hx
567
- r = _hx.post(url, timeout=10)
568
- r.raise_for_status()
569
- return r.json()
570
- except Exception:
571
- pass
572
- # urllib fallback — uses system CA bundle, works on Android/Termux
573
- import urllib.request as _ur, urllib.error as _ue
574
- req = _ur.Request(url, data=b"", method="POST")
575
- with _ur.urlopen(req, timeout=10) as resp:
576
- import json as _j
577
- return _j.loads(resp.read())
578
-
567
+ # 1. Get a pairing code using DoH-aware HTTP POST (bypasses Android stale DNS)
579
568
  try:
580
- data = _post_code(_guest_code_url(base_url))
569
+ data = _http_post(_guest_code_url(base_url))
581
570
  code = data["code"]
582
571
  ttl = data.get("expires_in", 600)
583
572
  except Exception as e:
@@ -600,7 +589,7 @@ def guest_serve(base_url: str) -> None:
600
589
  while True:
601
590
  try:
602
591
  ws_url = _guest_host_url(base_url, code)
603
- with _ws.connect(ws_url, open_timeout=15, close_timeout=5) as ws:
592
+ with _ws_connect(ws_url, open_timeout=15, close_timeout=5) as ws:
604
593
  ack = json.loads(ws.recv())
605
594
  if ack.get("type") == "error":
606
595
  print(f" {_R}✗{_RS} {ack.get('message')}", file=sys.stderr)
@@ -671,7 +660,6 @@ def guest_connect_shell(base_url: str, code: str) -> int:
671
660
  then opens a full PTY shell if approved.
672
661
  Returns the remote exit code.
673
662
  """
674
- import websockets.sync.client as _ws
675
663
  import select as _sel, signal
676
664
 
677
665
  code = code.strip().upper()
@@ -702,7 +690,7 @@ def guest_connect_shell(base_url: str, code: str) -> int:
702
690
 
703
691
  exit_code = 1
704
692
  try:
705
- with _ws.connect(ws_url, open_timeout=15, close_timeout=10) as ws:
693
+ with _ws_connect(ws_url, open_timeout=15, close_timeout=10) as ws:
706
694
  # First frame: could be error, waiting_approval, or connected
707
695
  first = json.loads(ws.recv())
708
696
  ftype = first.get("type")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.5.2
3
+ Version: 4.5.4
4
4
  Summary: Akash CLI — secure remote shell relay, file vault, and device management
5
5
  License: MIT
6
6
  Keywords: akash,cli,relay,remote-shell,vault
@@ -12,6 +12,7 @@ Classifier: Topic :: Utilities
12
12
  Requires-Python: >=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  Requires-Dist: httpx
15
+ Requires-Dist: certifi
15
16
  Requires-Dist: typer
16
17
  Requires-Dist: cryptography
17
18
  Requires-Dist: pydantic
@@ -1,6 +1,7 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  akashcli/__init__.py
4
+ akashcli/_net.py
4
5
  akashcli/auth.py
5
6
  akashcli/cli.py
6
7
  akashcli/client.py
@@ -1,4 +1,5 @@
1
1
  httpx
2
+ certifi
2
3
  typer
3
4
  cryptography
4
5
  pydantic
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "akashcli"
3
- version = "4.5.2"
3
+ version = "4.5.4"
4
4
  description = "Akash CLI — secure remote shell relay, file vault, and device management"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -15,6 +15,7 @@ classifiers = [
15
15
  ]
16
16
  dependencies = [
17
17
  "httpx",
18
+ "certifi",
18
19
  "typer",
19
20
  "cryptography",
20
21
  "pydantic",
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