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.
- {akashcli-4.6.2 → akashcli-4.6.4}/PKG-INFO +2 -3
- akashcli-4.6.4/akashcli/__init__.py +7 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/_net.py +2 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/cli.py +48 -36
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/exec.py +31 -23
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/relay.py +479 -126
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/restore.py +144 -56
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/terminal.py +150 -22
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/uploader.py +36 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/PKG-INFO +2 -3
- {akashcli-4.6.2 → akashcli-4.6.4}/pyproject.toml +7 -3
- akashcli-4.6.2/akashcli/__init__.py +0 -6
- {akashcli-4.6.2 → akashcli-4.6.4}/README.md +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/auth.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/client.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/config.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/crypto.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/env_manager.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/exceptions.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/integrity.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/logger.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/models.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/p2p.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/packages.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/storage.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli/utils.py +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.6.2 → akashcli-4.6.4}/akashcli.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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:
|
|
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
|
-
|
|
98
|
-
sig =
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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()
|