akashcli 4.4.1__tar.gz → 4.5.1__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 (31) hide show
  1. {akashcli-4.4.1 → akashcli-4.5.1}/PKG-INFO +1 -1
  2. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/cli.py +54 -24
  3. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/relay.py +277 -0
  4. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli.egg-info/PKG-INFO +1 -1
  5. {akashcli-4.4.1 → akashcli-4.5.1}/pyproject.toml +1 -1
  6. {akashcli-4.4.1 → akashcli-4.5.1}/README.md +0 -0
  7. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/__init__.py +0 -0
  8. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/auth.py +0 -0
  9. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/client.py +0 -0
  10. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/config.py +0 -0
  11. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/crypto.py +0 -0
  12. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/env_manager.py +0 -0
  13. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/exceptions.py +0 -0
  14. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/exec.py +0 -0
  15. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/integrity.py +0 -0
  16. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/logger.py +0 -0
  17. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/models.py +0 -0
  18. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/p2p.py +0 -0
  19. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/packages.py +0 -0
  20. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/restore.py +0 -0
  21. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/snapshot_crypto.py +0 -0
  22. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/storage.py +0 -0
  23. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/terminal.py +0 -0
  24. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/uploader.py +0 -0
  25. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli/utils.py +0 -0
  26. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli.egg-info/SOURCES.txt +0 -0
  27. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli.egg-info/dependency_links.txt +0 -0
  28. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli.egg-info/entry_points.txt +0 -0
  29. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli.egg-info/requires.txt +0 -0
  30. {akashcli-4.4.1 → akashcli-4.5.1}/akashcli.egg-info/top_level.txt +0 -0
  31. {akashcli-4.4.1 → akashcli-4.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.4.1
3
+ Version: 4.5.1
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
@@ -179,9 +179,11 @@ def _print_help():
179
179
  f" {C}pull{RS} {DM}<CODE>{RS} restore on any machine — no login needed",
180
180
  f"",
181
181
  f" {DM}REMOTE SHELL{RS}",
182
- f" {C}shell{RS} {DM}[--device NAME]{RS} open a shell here, or on another device",
182
+ f" {C}shell{RS} {DM}[--device NAME]{RS} open a shell here, on a device, or local",
183
+ f" {C}shell{RS} {DM}--code A3F7B2{RS} connect via one-time pairing code (no login)",
183
184
  f" {C}run{RS} {DM}<CMD> [--device NAME]{RS} run a command locally or on a device",
184
- f" {C}serve{RS} {DM}[--label NAME]{RS} make this device reachable for shell/run",
185
+ f" {C}serve{RS} {DM}[--share]{RS} share this shell (prints a pairing code)",
186
+ f" {C}serve{RS} {DM}[--label NAME]{RS} register device permanently (needs login)",
185
187
  f" {C}devices{RS} {DM}[--revoke NAME]{RS} list or revoke enrolled devices",
186
188
  f"",
187
189
  f" {DM}FILES{RS}",
@@ -1142,21 +1144,20 @@ def shell(
1142
1144
  reauth: bool = typer.Option(False, "--reauth", help="Force re-authentication"),
1143
1145
  device: Optional[str] = typer.Option(None, "--device", "-d",
1144
1146
  help="Device label to connect to (set up with: akash serve --label NAME)"),
1147
+ code: Optional[str] = typer.Option(None, "--code", "-c",
1148
+ help="Pairing code from `akash serve` — no login needed"),
1145
1149
  ):
1146
- """Open a shell — local, server, or on any enrolled device.
1150
+ """Open a shell — local, on a device, or via a one-time pairing code.
1147
1151
 
1148
1152
  No login needed:
1149
1153
  akash shell open a local bash shell
1154
+ akash serve get a pairing code, share it
1155
+ akash shell --code A3F7B2 connect using that code
1150
1156
 
1151
- With login (akash login):
1152
- akash shell full server shell (persistent, coloured)
1153
- akash shell --device termux relay shell on your phone
1154
- akash shell --device laptop relay shell on your laptop
1155
- akash shell --device auto-pick (works if only 1 device enrolled)
1156
-
1157
- To make a device reachable:
1158
- 1. On that device: akash login --label termux && akash serve
1159
- 2. From anywhere: akash shell --device termux
1157
+ With login:
1158
+ akash shell full server shell
1159
+ akash shell --device termux relay shell on enrolled device
1160
+ akash shell --device auto-pick enrolled device
1160
1161
  """
1161
1162
  from .exec import acquire_exec_token, run_shell, _clear_cached_token
1162
1163
  import shutil as _sh, subprocess as _sp
@@ -1164,13 +1165,30 @@ def shell(
1164
1165
  try:
1165
1166
  api_key, base = _resolve_client(url)
1166
1167
 
1168
+ # ── Pairing code path (no login needed) ───────────────────────────
1169
+ if code is not None:
1170
+ from .relay import guest_connect_shell
1171
+ c = code.strip().upper()
1172
+ _p()
1173
+ _info(f"connecting with code {_cy(c)}…")
1174
+ try:
1175
+ exit_code = guest_connect_shell(base, c)
1176
+ except RuntimeError as e:
1177
+ _err(str(e))
1178
+ raise typer.Exit(1)
1179
+ _p()
1180
+ _info("session closed")
1181
+ if exit_code != 0:
1182
+ raise typer.Exit(exit_code)
1183
+ return
1184
+
1167
1185
  # ── --device flag but no key ──────────────────────────────────────
1168
1186
  if device is not None and not api_key:
1169
1187
  _warn("relay requires an API key.")
1170
1188
  _p()
1171
1189
  _info(f"To connect to {_cy(device.strip() or 'a device')} you need to log in first:")
1172
1190
  _info(f" {_cy('akash login')} save your API key on this machine")
1173
- _info(f" {_cy('akash serve')} run on the target device to make it reachable")
1191
+ _info(f" Or use a pairing code: {_cy('akash serve')} → {_cy('akash shell --code <CODE>')}")
1174
1192
  _p()
1175
1193
  raise typer.Exit(1)
1176
1194
 
@@ -1337,21 +1355,33 @@ def serve(
1337
1355
  daemon: bool = typer.Option(False, "--daemon", help="Run in background"),
1338
1356
  status: bool = typer.Option(False, "--status", help="Show serve daemon status"),
1339
1357
  stop: bool = typer.Option(False, "--stop", help="Stop background serve daemon"),
1358
+ share: bool = typer.Option(False, "--share", help="Guest mode: get a one-time code, no login needed"),
1340
1359
  ):
1341
- """Make this device reachable for remote shell and run.
1360
+ """Make this device reachable for remote shell — with or without login.
1342
1361
 
1343
- Run this once on a device to register it. Then from any other
1344
- machine: akash shell --device NAME or akash run --device NAME
1362
+ No login needed (one-time pairing code):
1363
+ akash serve --share prints a code, share it with anyone
1364
+ akash shell --code A3F7B2 they connect using that code
1345
1365
 
1346
- Examples:
1347
- akash serve start in foreground (Ctrl-C to stop)
1366
+ With login (permanent, device enrolled):
1367
+ akash serve register this device permanently
1348
1368
  akash serve --daemon run as background daemon
1349
- akash serve --status check if daemon is running
1350
- akash serve --stop stop the daemon
1351
1369
  akash serve --label myphone use a custom label
1370
+ akash serve --status check daemon status
1371
+ akash serve --stop stop daemon
1352
1372
  """
1353
1373
  from .relay import serve as _serve, serve_status, serve_stop
1354
1374
 
1375
+ # ── Guest / no-login mode ─────────────────────────────────────────────
1376
+ if share:
1377
+ from .relay import guest_serve
1378
+ _, base = _resolve_client(url)
1379
+ try:
1380
+ guest_serve(base)
1381
+ except KeyboardInterrupt:
1382
+ pass
1383
+ return
1384
+
1355
1385
  if status:
1356
1386
  st = serve_status()
1357
1387
  if st["running"]:
@@ -1380,11 +1410,11 @@ def serve(
1380
1410
 
1381
1411
  if not api_key:
1382
1412
  _p()
1383
- _warn("not logged in — can't register with relay server.")
1413
+ _warn("not logged in.")
1414
+ _p()
1415
+ _info(f"For a quick one-time session (no login): {_cy('akash serve --share')}")
1384
1416
  _p()
1385
- _info("Run this first:")
1386
- print(f" {_cy('akash login --label myphone')} {_DM}# name this device{_RS}")
1387
- print(f" {_cy('akash serve')} {_DM}# then run this{_RS}")
1417
+ _info(f"To set up permanently: {_cy('akash login --label myphone')} then {_cy('akash serve')}")
1388
1418
  _p()
1389
1419
  raise typer.Exit(1)
1390
1420
 
@@ -535,3 +535,280 @@ def serve_stop() -> bool:
535
535
  return True
536
536
  except Exception:
537
537
  return False
538
+
539
+
540
+ # ── Guest relay (no API key — pairing code) ───────────────────────────────────
541
+
542
+ def _guest_code_url(base: str) -> str:
543
+ return f"{base.rstrip('/')}/api/guest/relay/code"
544
+
545
+ def _guest_host_url(base: str, code: str) -> str:
546
+ return f"{_ws_base(base)}/api/guest/relay/host?code={code}"
547
+
548
+ def _guest_join_url(base: str, code: str) -> str:
549
+ return f"{_ws_base(base)}/api/guest/relay/join?code={code}"
550
+
551
+
552
+ def guest_serve(base_url: str) -> None:
553
+ """
554
+ Host a shell session with no API key.
555
+ Requests a pairing code from the server, prints it, then waits for
556
+ someone to connect with: akash shell --code <CODE>
557
+
558
+ Security: every incoming connection shows the guest IP + fingerprint
559
+ and asks the host to approve before the shell is bridged.
560
+ """
561
+ import httpx
562
+ import websockets.sync.client as _ws
563
+
564
+ # 1. Get a pairing code
565
+ try:
566
+ r = httpx.post(_guest_code_url(base_url), timeout=10)
567
+ r.raise_for_status()
568
+ code = r.json()["code"]
569
+ ttl = r.json().get("expires_in", 600)
570
+ except Exception as e:
571
+ print(f" {_R}✗{_RS} could not get pairing code: {e}", file=sys.stderr)
572
+ return
573
+
574
+ print()
575
+ print(f" {_G}◉{_RS} guest relay ready")
576
+ print()
577
+ print(f" {_DM}Pairing code:{_RS} {_C}{code}{_RS}")
578
+ print()
579
+ print(f" {_DM}On another machine:{_RS} {_C}akash shell --code {code}{_RS}")
580
+ print()
581
+ print(f" {_DM}Code expires in {ttl // 60} min · Ctrl-C to stop{_RS}")
582
+ print(f" {_DM}You will be asked to approve each connection before it gets shell access.{_RS}")
583
+ print()
584
+
585
+ # 2. Connect to host endpoint — handle knock/allow/deny then serve shell
586
+ while True:
587
+ try:
588
+ ws_url = _guest_host_url(base_url, code)
589
+ with _ws.connect(ws_url, open_timeout=15, close_timeout=5) as ws:
590
+ ack = json.loads(ws.recv())
591
+ if ack.get("type") == "error":
592
+ print(f" {_R}✗{_RS} {ack.get('message')}", file=sys.stderr)
593
+ return
594
+
595
+ print(f" {_DM}waiting for connection…{_RS}", flush=True)
596
+
597
+ # Wait for either a knock (someone wants in) or a ping
598
+ while True:
599
+ try:
600
+ raw = ws.recv(timeout=35)
601
+ except Exception:
602
+ # Timeout or disconnect — break to reconnect loop
603
+ break
604
+ frame = json.loads(raw)
605
+ ftype = frame.get("type")
606
+
607
+ if ftype == "ping":
608
+ ws.send(json.dumps({"type": "pong"}))
609
+ continue
610
+
611
+ if ftype == "knock":
612
+ guest_ip = frame.get("ip", "unknown")
613
+ fp = frame.get("fingerprint", "????????")
614
+ print()
615
+ print(f" {_Y}⚡{_RS} Incoming connection request")
616
+ print(f" {_DM}From:{_RS} {_C}{guest_ip}{_RS}")
617
+ print(f" {_DM}Fingerprint:{_RS} {_C}{fp}{_RS}")
618
+ print()
619
+
620
+ # Prompt host — default is N (deny)
621
+ try:
622
+ sys.stdout.write(f" Allow? [y/N]: ")
623
+ sys.stdout.flush()
624
+ answer = sys.stdin.readline().strip().lower()
625
+ except (EOFError, KeyboardInterrupt):
626
+ answer = "n"
627
+
628
+ if answer == "y":
629
+ ws.send(json.dumps({"type": "allow"}))
630
+ print(f" {_G}✓{_RS} connection allowed — starting shell…")
631
+ print()
632
+ # Now serve shell frames
633
+ _run_relay_device(ws, f"guest:{code}")
634
+ print(f"\n {_DM}session ended — waiting for next connection…{_RS}\n")
635
+ else:
636
+ ws.send(json.dumps({"type": "deny"}))
637
+ print(f" {_R}✗{_RS} connection denied")
638
+ print()
639
+ # Loop back — still waiting on same code
640
+ continue
641
+
642
+ # Any other frame — ignore and keep waiting
643
+ continue
644
+
645
+ except KeyboardInterrupt:
646
+ print(f"\n {_DM}guest serve stopped{_RS}")
647
+ break
648
+ except Exception as exc:
649
+ print(f" {_Y}!{_RS} relay disconnected ({exc}) — reconnecting in 3s…")
650
+ time.sleep(3)
651
+
652
+
653
+ def guest_connect_shell(base_url: str, code: str) -> int:
654
+ """
655
+ Connect to a guest host by pairing code — no API key needed.
656
+ Handles the approval handshake: shows a spinner while the host decides,
657
+ then opens a full PTY shell if approved.
658
+ Returns the remote exit code.
659
+ """
660
+ import websockets.sync.client as _ws
661
+ import select as _sel, signal
662
+
663
+ code = code.strip().upper()
664
+ ws_url = _guest_join_url(base_url, code)
665
+
666
+ from .exec import _get_terminal_size
667
+ rows, cols = _get_terminal_size()
668
+
669
+ _raw_ok = False
670
+ _old_settings = None
671
+ _stdin_fd = -1
672
+ try:
673
+ import tty, termios as _term
674
+ _stdin_fd = sys.stdin.fileno()
675
+ _old_settings = _term.tcgetattr(_stdin_fd)
676
+ tty.setraw(_stdin_fd)
677
+ _raw_ok = True
678
+ except Exception:
679
+ pass
680
+
681
+ def _restore():
682
+ if _raw_ok and _old_settings is not None:
683
+ try:
684
+ import termios as _t
685
+ _t.tcsetattr(_stdin_fd, _t.TCSADRAIN, _old_settings)
686
+ except Exception:
687
+ pass
688
+
689
+ exit_code = 1
690
+ try:
691
+ with _ws.connect(ws_url, open_timeout=15, close_timeout=10) as ws:
692
+ # First frame: could be error, waiting_approval, or connected
693
+ first = json.loads(ws.recv())
694
+ ftype = first.get("type")
695
+
696
+ if ftype == "error":
697
+ _restore()
698
+ raise RuntimeError(first.get("message", "relay error"))
699
+
700
+ if ftype == "waiting_approval":
701
+ fp = first.get("fingerprint", "")
702
+ _restore() # restore terminal so we can print cleanly
703
+ print(f" {_Y}…{_RS} waiting for host to approve this connection…")
704
+ if fp:
705
+ print(f" {_DM}Your fingerprint: {_C}{fp}{_RS}")
706
+ print(f" {_DM}(host has 30 seconds to respond){_RS}")
707
+ print()
708
+ # Wait for next frame: connected or denied
709
+ try:
710
+ second = json.loads(ws.recv(timeout=35))
711
+ except Exception:
712
+ raise RuntimeError("Timed out waiting for host approval.")
713
+ if second.get("type") == "denied":
714
+ raise RuntimeError("Host declined the connection.")
715
+ if second.get("type") != "connected":
716
+ raise RuntimeError(second.get("message", "unexpected response from host"))
717
+ # Re-enter raw mode for the PTY session
718
+ try:
719
+ import tty as _tty2, termios as _term2
720
+ _old_settings = _term2.tcgetattr(_stdin_fd)
721
+ _tty2.setraw(_stdin_fd)
722
+ _raw_ok = True
723
+ except Exception:
724
+ pass
725
+ elif ftype != "connected":
726
+ _restore()
727
+ raise RuntimeError(first.get("message", "unexpected server response"))
728
+
729
+ ws.send(json.dumps({
730
+ "type": "pty_run",
731
+ "command": os.environ.get("SHELL", "/bin/bash"),
732
+ "rows": rows, "cols": cols,
733
+ }))
734
+
735
+ stop = threading.Event()
736
+
737
+ def _stdin_pump():
738
+ try:
739
+ while not stop.is_set():
740
+ if _stdin_fd >= 0:
741
+ r = _sel.select([_stdin_fd], [], [], 0.05)[0]
742
+ if not r:
743
+ continue
744
+ chunk = os.read(_stdin_fd, 256)
745
+ else:
746
+ chunk = sys.stdin.buffer.read(1)
747
+ if not chunk:
748
+ break
749
+ ws.send(json.dumps({
750
+ "type": "pty_in",
751
+ "data": base64.b64encode(chunk).decode(),
752
+ }))
753
+ except Exception:
754
+ pass
755
+
756
+ def _on_resize(signum, _frame):
757
+ r, c = _get_terminal_size()
758
+ try:
759
+ ws.send(json.dumps({"type": "resize", "rows": r, "cols": c}))
760
+ except Exception:
761
+ pass
762
+ try:
763
+ signal.signal(signal.SIGWINCH, _on_resize)
764
+ except Exception:
765
+ pass
766
+
767
+ t = threading.Thread(target=_stdin_pump, daemon=True)
768
+ t.start()
769
+
770
+ while True:
771
+ try:
772
+ raw = ws.recv()
773
+ except Exception:
774
+ break
775
+ try:
776
+ frame = json.loads(raw)
777
+ except Exception:
778
+ continue
779
+ ftype = frame.get("type")
780
+ if ftype == "pty_out":
781
+ data = base64.b64decode(frame["data"])
782
+ if data:
783
+ sys.stdout.buffer.write(data)
784
+ sys.stdout.buffer.flush()
785
+ elif ftype == "exit":
786
+ exit_code = frame.get("code", 0)
787
+ stop.set()
788
+ break
789
+ elif ftype == "error":
790
+ stop.set()
791
+ _restore()
792
+ print(f"\r\n{_R}[error]{_RS} {frame.get('message','?')}", file=sys.stderr)
793
+ exit_code = 1
794
+ break
795
+ elif ftype == "disconnected":
796
+ stop.set()
797
+ break
798
+ elif ftype == "ping":
799
+ try:
800
+ ws.send(json.dumps({"type": "pong"}))
801
+ except Exception:
802
+ break
803
+
804
+ except KeyboardInterrupt:
805
+ pass
806
+ finally:
807
+ stop.set() if 'stop' in dir() else None
808
+ _restore()
809
+ try:
810
+ signal.signal(signal.SIGWINCH, signal.SIG_DFL)
811
+ except Exception:
812
+ pass
813
+
814
+ return exit_code
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: akashcli
3
- Version: 4.4.1
3
+ Version: 4.5.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "akashcli"
3
- version = "4.4.1"
3
+ version = "4.5.1"
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"
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