akashcli 4.6.2__tar.gz → 4.6.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 (33) hide show
  1. {akashcli-4.6.2 → akashcli-4.6.4}/PKG-INFO +2 -3
  2. akashcli-4.6.4/akashcli/__init__.py +7 -0
  3. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/_net.py +2 -0
  4. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/cli.py +48 -36
  5. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/exec.py +31 -23
  6. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/relay.py +479 -126
  7. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/restore.py +144 -56
  8. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/terminal.py +150 -22
  9. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/uploader.py +36 -0
  10. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/PKG-INFO +2 -3
  11. {akashcli-4.6.2 → akashcli-4.6.4}/pyproject.toml +7 -3
  12. akashcli-4.6.2/akashcli/__init__.py +0 -6
  13. {akashcli-4.6.2 → akashcli-4.6.4}/README.md +0 -0
  14. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/auth.py +0 -0
  15. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/client.py +0 -0
  16. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/config.py +0 -0
  17. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/crypto.py +0 -0
  18. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/env_manager.py +0 -0
  19. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/exceptions.py +0 -0
  20. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/integrity.py +0 -0
  21. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/logger.py +0 -0
  22. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/models.py +0 -0
  23. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/p2p.py +0 -0
  24. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/packages.py +0 -0
  25. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/snapshot_crypto.py +0 -0
  26. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/storage.py +0 -0
  27. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/utils.py +0 -0
  28. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/SOURCES.txt +0 -0
  29. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/dependency_links.txt +0 -0
  30. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/entry_points.txt +0 -0
  31. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/requires.txt +0 -0
  32. {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/top_level.txt +0 -0
  33. {akashcli-4.6.2 → akashcli-4.6.4}/setup.cfg +0 -0
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.6.2
3
+ Version: 4.6.4
4
4
  Summary: Akash CLI — secure remote shell relay, file vault, and device management
5
- License: MIT
5
+ License-Expression: MIT
6
6
  Keywords: akash,cli,relay,remote-shell,vault
7
7
  Classifier: Programming Language :: Python :: 3
8
- Classifier: License :: OSI Approved :: MIT License
9
8
  Classifier: Operating System :: OS Independent
10
9
  Classifier: Environment :: Console
11
10
  Classifier: Topic :: Utilities
@@ -0,0 +1,7 @@
1
+ try:
2
+ from importlib.metadata import version
3
+ __version__ = version("akashcli")
4
+ except Exception:
5
+ __version__ = "4.6.4"
6
+
7
+ AKASH_VERSION = __version__
@@ -112,6 +112,8 @@ def ws_connect(url: str, **kwargs):
112
112
 
113
113
  sock_timeout = kwargs.pop("_sock_timeout", 15)
114
114
  sock = make_socket(host, port, timeout=sock_timeout)
115
+ # Remove socket timeout so the WebSocket connection remains open permanently
116
+ sock.settimeout(None)
115
117
 
116
118
  # legacy=True: allow calling connect() outside of a `with` block.
117
119
  # websockets ≥ 13 requires context manager usage by default;
@@ -141,6 +141,13 @@ def _upload_with_progress(client: AkashClient, tmp_path: str, password=None) ->
141
141
  # `app` — primary commands (backup / restore / pull / run / shell …)
142
142
  # `cloud_app` — file management (akash cloud upload/files/stream/delete/manage/bulk)
143
143
 
144
+
145
+ def _version_callback(value: bool):
146
+ if value:
147
+ from . import __version__
148
+ print("akashcli v" + __version__)
149
+ raise typer.Exit(0)
150
+
144
151
  app = typer.Typer(
145
152
  invoke_without_command=True,
146
153
  rich_markup_mode=None,
@@ -150,6 +157,9 @@ app = typer.Typer(
150
157
 
151
158
  @app.callback(invoke_without_command=True)
152
159
  def _main(ctx: typer.Context,
160
+ version: bool = typer.Option(False, "--version", "-v", is_eager=True, expose_value=False,
161
+ callback=lambda v: (print("akashcli v1.5.0"), raise_exit()) if v else None,
162
+ help="Show version and exit."),
153
163
  help: bool = typer.Option(False, "--help", "-h", is_eager=True, expose_value=False,
154
164
  callback=lambda v: (_print_help(), raise_exit()) if v else None,
155
165
  help="Show this message and exit.")):
@@ -256,6 +266,24 @@ def login(
256
266
 
257
267
  # ── akash whoami ─────────────────────────────────────────────────────────────
258
268
 
269
+
270
+ @app.command()
271
+ def version(url: Optional[str] = typer.Option(None, "--url", help="Server URL to check")):
272
+ """Show akash CLI version and connected server version."""
273
+ from . import __version__
274
+ ver_tag = "v" + __version__
275
+ print(f"\n {_C}◈{_RS} {_W}akashcli{_RS} {_cy(ver_tag)}")
276
+ try:
277
+ _, base = _resolve_client(url, require_key=False)
278
+ resp = httpx.get(f"{base}/health", timeout=3.0)
279
+ if resp.status_code == 200:
280
+ s_ver = resp.json().get("version", "unknown")
281
+ srv_tag = "(" + s_ver + ")"
282
+ print(f" {_DM}server {_RS}{_cy(base)} {_dim(srv_tag)}")
283
+ except Exception:
284
+ pass
285
+ print()
286
+
259
287
  @app.command()
260
288
  def whoami():
261
289
  """Show current account and storage usage."""
@@ -842,41 +870,10 @@ def pull(
842
870
  except KeyError:
843
871
  pass
844
872
 
845
- for member in tar.getmembers():
846
- name = member.name
847
- if name in ("MANIFEST.json", "manifest_meta.json", "akash.pkgs") or not member.isfile():
848
- continue
849
-
850
- dest = None
851
- if name.startswith("home/"):
852
- dest = home / name[len("home/"):]
853
- elif name.startswith("abs/"):
854
- dest = Path(name[len("abs"):])
855
- elif name.startswith("extra/"):
856
- dest = home / name[len("extra/"):]
857
- else:
858
- skipped_list.append(name)
859
- continue
860
-
861
- if dry_run:
862
- restored_list.append(str(dest))
863
- continue
864
-
865
- try:
866
- dest.parent.mkdir(parents=True, exist_ok=True)
867
- file_data = tar.extractfile(member)
868
- if file_data is None:
869
- skipped_list.append(str(dest))
870
- continue
871
- dest.write_bytes(file_data.read())
872
- try:
873
- os.chmod(dest, member.mode)
874
- except OSError:
875
- pass
876
- restored_list.append(str(dest))
877
- except (PermissionError, OSError) as e:
878
- skipped_list.append(f"{dest} ({e})")
879
-
873
+ from .restore import validate_and_extract_members
874
+ restored_list, skipped_list = validate_and_extract_members(
875
+ tar, target_home=home, dry_run=dry_run
876
+ )
880
877
  except Exception as e:
881
878
  _err(f"extraction failed: {e}")
882
879
  raise typer.Exit(1)
@@ -1250,7 +1247,8 @@ def shell(
1250
1247
 
1251
1248
  @app.command()
1252
1249
  def devices(
1253
- revoke: Optional[str] = typer.Option(None, "--revoke", "-r", help="Revoke (unenroll) a device by label"),
1250
+ revoke: Optional[str] = typer.Option(None, "--revoke", "-r", help="Revoke (unenroll) a device by label"),
1251
+ authorize: Optional[str] = typer.Option(None, "--authorize", "-a", help="Grant server execution access to device"),
1254
1252
  raw: bool = typer.Option(False, "--json", help="Raw JSON output"),
1255
1253
  ):
1256
1254
  """List enrolled devices — or revoke one.
@@ -1292,6 +1290,20 @@ def devices(
1292
1290
 
1293
1291
  _hdrs = {"x-api-key": api_key, "Origin": "https://akash.cli"}
1294
1292
 
1293
+ if authorize:
1294
+ try:
1295
+ import httpx as _hx
1296
+ resp = _hx.post(
1297
+ f"{base}/api/exec/devices/{authorize}/authorize",
1298
+ headers=_hdrs, timeout=10,
1299
+ )
1300
+ resp.raise_for_status()
1301
+ _ok(f"authorized {_ye(authorize)} for server execution")
1302
+ except Exception as e:
1303
+ _err(f"authorize failed: {e}")
1304
+ raise typer.Exit(1)
1305
+ return
1306
+
1295
1307
  if revoke:
1296
1308
  try:
1297
1309
  import httpx as _hx
@@ -93,10 +93,21 @@ def _get_machine_key() -> bytes:
93
93
  return key
94
94
 
95
95
 
96
+ def _get_ed25519_key():
97
+ from cryptography.hazmat.primitives.asymmetric import ed25519 as _ed25519
98
+ raw = _get_machine_key()
99
+ return _ed25519.Ed25519PrivateKey.from_private_bytes(raw)
100
+
101
+ def _get_public_key_b64() -> str:
102
+ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
103
+ pub = _get_ed25519_key().public_key()
104
+ raw = pub.public_bytes(Encoding.Raw, PublicFormat.Raw)
105
+ return base64.b64encode(raw).decode("utf-8")
106
+
96
107
  def _sign_challenge(challenge: str) -> str:
97
- key = _get_machine_key()
98
- sig = _hmac.new(key, challenge.encode(), hashlib.sha256).digest()
99
- return base64.b64encode(sig).decode()
108
+ priv = _get_ed25519_key()
109
+ sig = priv.sign(challenge.encode("utf-8"))
110
+ return base64.b64encode(sig).decode("utf-8")
100
111
 
101
112
 
102
113
  def _device_id() -> str:
@@ -235,14 +246,6 @@ def _session_info() -> dict:
235
246
  # ── Authentication ────────────────────────────────────────────────────────────
236
247
 
237
248
  def acquire_exec_token(api_key: str, base_url: str, controller: bool = False) -> str:
238
- """
239
- Obtain a short-lived exec JWT.
240
-
241
- controller=True — skip device fingerprint enrollment (used by relay
242
- connect / run / shell). Just authenticates as owner.
243
- controller=False — full challenge/sign/enroll flow (used by akash serve).
244
- """
245
- # Patch socket DNS for this host — bypasses Android stale DNS cache
246
249
  from urllib.parse import urlparse as _up
247
250
  _patch_dns(_up(base_url).hostname or "")
248
251
 
@@ -251,22 +254,27 @@ def acquire_exec_token(api_key: str, base_url: str, controller: bool = False) ->
251
254
  return token
252
255
 
253
256
  with httpx.Client(base_url=base_url, timeout=10) as client:
254
- if controller:
255
- # Controller path: no device fingerprint needed — just owner auth.
256
- return _auth_key_only(client, api_key, base_url)
257
-
258
- ch = client.post("/api/exec/challenge",
259
- headers={"x-api-key": api_key, "Origin": "https://akash.cli"})
260
- if ch.status_code == 404:
261
- return _auth_key_only(client, api_key, base_url)
257
+ ch = client.post(
258
+ "/api/exec/challenge",
259
+ headers={"x-api-key": api_key, "Origin": "https://akash.cli"}
260
+ )
262
261
  ch.raise_for_status()
263
262
  nonce = ch.json()["nonce"]
264
263
 
265
264
  sig = _sign_challenge(nonce)
266
- ar = client.post("/api/exec/auth",
267
- headers={"x-api-key": api_key, "Origin": "https://akash.cli"},
268
- json={"nonce": nonce, "signature": sig,
269
- "device_id": _device_id(), "label": _device_label()})
265
+ pub_key = _get_public_key_b64()
266
+ label = _device_label()
267
+
268
+ ar = client.post(
269
+ "/api/exec/auth",
270
+ headers={"x-api-key": api_key, "Origin": "https://akash.cli"},
271
+ json={
272
+ "nonce": nonce,
273
+ "signature": sig,
274
+ "public_key": pub_key,
275
+ "label": label,
276
+ }
277
+ )
270
278
  if ar.status_code == 403:
271
279
  raise PermissionError(ar.json().get("detail", "Access denied."))
272
280
  ar.raise_for_status()