godfather-cli 2.1.0__tar.gz → 2.3.0__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 (26) hide show
  1. {godfather_cli-2.1.0/godfather_cli.egg-info → godfather_cli-2.3.0}/PKG-INFO +35 -14
  2. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/README.md +34 -13
  3. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli/__init__.py +1 -1
  4. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli/auth.py +105 -12
  5. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli/cli.py +148 -42
  6. godfather_cli-2.3.0/godfather_cli/login_server.py +72 -0
  7. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli/picker.py +20 -13
  8. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli/pod_manager.py +111 -14
  9. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli/ssh_connector.py +96 -12
  10. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli/update_checker.py +18 -20
  11. {godfather_cli-2.1.0 → godfather_cli-2.3.0/godfather_cli.egg-info}/PKG-INFO +35 -14
  12. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli.egg-info/SOURCES.txt +5 -1
  13. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/pyproject.toml +14 -2
  14. godfather_cli-2.3.0/tests/test_cli.py +262 -0
  15. godfather_cli-2.3.0/tests/test_login.py +151 -0
  16. godfather_cli-2.3.0/tests/test_sessions.py +109 -0
  17. godfather_cli-2.3.0/tests/test_vscode.py +98 -0
  18. godfather_cli-2.1.0/tests/test_cli.py +0 -102
  19. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/LICENSE +0 -0
  20. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/MANIFEST.in +0 -0
  21. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli/ui.py +1 -1
  22. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli.egg-info/dependency_links.txt +0 -0
  23. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli.egg-info/entry_points.txt +0 -0
  24. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli.egg-info/requires.txt +0 -0
  25. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/godfather_cli.egg-info/top_level.txt +0 -0
  26. {godfather_cli-2.1.0 → godfather_cli-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: godfather-cli
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: Free compute for ASU students: connect to AI Society at ASU GPU pods
5
5
  Author-email: AI Society ASU <theaisocietyasu@gmail.com>
6
6
  License: MIT
@@ -62,10 +62,13 @@ pip install -e .
62
62
 
63
63
  1. Run `godfather` with no arguments. On first run you won't be logged in
64
64
  yet, so it'll walk you into the login flow.
65
- 2. It opens the AI Society platform's sign-in page in your browser (and
66
- prints the link). Sign in with Discord and copy the token shown there.
67
- 3. Paste the token back into the terminal. The CLI checks it with the
68
- platform and stores it in `~/.godfather/config.json`.
65
+ 2. It opens the sign-in page in your browser and prints the link. Sign in
66
+ with Discord. The browser sends the login back to the CLI, which checks it
67
+ and stores the token in `~/.godfather/config.json`.
68
+ 3. If the browser cannot reach the CLI (no browser on this computer, an
69
+ older server, or no sign-in within 5 minutes), the CLI asks you to paste
70
+ the token that the sign-in page shows. `godfather auth --paste` goes
71
+ straight to that, for a browser on another computer.
69
72
  4. From the menu (or `godfather connect`), pick a pod. The CLI creates an SSH
70
73
  key on your machine the first time, asks the server for a 12-hour
71
74
  certificate for that pod, and opens the connection.
@@ -81,12 +84,14 @@ Windows 10+ ship it.
81
84
  ### Interactive menu
82
85
 
83
86
  Running `godfather` with no arguments opens a menu. Use the arrow keys to
84
- choose Connect to a pod, List pods, Status, Log out or Exit, and press Enter.
85
- Ctrl-C quits.
87
+ choose Connect to a pod, List pods, Sessions, Set up VS Code, Status, Log out or Exit, and
88
+ press Enter. Ctrl-C quits.
86
89
 
87
90
  Connect to a pod shows a list of your pods with their status, access and
88
91
  pod ID. A pod that is not running shows with the reason and cannot be
89
92
  chosen. If only one pod is running, the CLI asks for a yes instead.
93
+ If none of your pods is running, the CLI shows the next session on your
94
+ pods. A pod starts 10 minutes before its session.
90
95
 
91
96
  When input is not a terminal (for example in a script), the menu and the pod
92
97
  list use numbered prompts.
@@ -97,8 +102,13 @@ list use numbered prompts.
97
102
  godfather list # List pods you can connect to
98
103
  godfather connect # Connect to a pod, picking from a list
99
104
  godfather connect <pod-id> # Connect to a specific pod
105
+ godfather connect --admin # Ask for a root shell (officers, when pod admin is on)
106
+ godfather sessions # Upcoming sessions on your pods, in local time
107
+ godfather vscode # Set up VS Code Remote-SSH for a pod, picking from a list
108
+ godfather vscode <pod-id> # The same for one pod; run it again to renew the certificate
100
109
  godfather status # Show server, org, login, token expiry and version
101
- godfather auth # Log in, or refresh an expired session
110
+ godfather auth # Log in with Discord in your browser
111
+ godfather auth --paste # Log in by pasting the token
102
112
  godfather logout # Clear the stored session
103
113
  godfather update # Update the CLI to the latest version
104
114
 
@@ -106,6 +116,16 @@ godfather update # Update the CLI to the latest version
106
116
  godfather --api-url https://platform.example.com --org soda list
107
117
  ```
108
118
 
119
+ ### VS Code
120
+
121
+ `godfather vscode` sets up a pod for the VS Code Remote - SSH extension:
122
+
123
+ 1. It gets a 12-hour certificate for the pod and saves it as `~/.godfather/ssh/<pod-id>-cert.pub`.
124
+ 2. It writes a `Host godfather-<pod-id>` block to `~/.godfather/ssh/config` and adds one `Include` line for that file at the top of `~/.ssh/config`.
125
+ 3. If the `code` command is on your PATH, it opens VS Code on `/workspace/users/<you>` on the pod. Otherwise run **Remote-SSH: Connect to Host...** in VS Code and pick `godfather-<pod-id>`.
126
+
127
+ VS Code runs as your pod account (`godfather_<you>`), with the same files and limits as `godfather connect`. After 12 hours VS Code cannot reconnect; run `godfather vscode <pod-id>` again. The pod must run image 2.3.0 or later. `godfather vscode --admin` gives officers a root login on a pod with pod admin on.
128
+
109
129
  ## Files it keeps
110
130
 
111
131
  Everything lives in `~/.godfather/`:
@@ -113,6 +133,7 @@ Everything lives in `~/.godfather/`:
113
133
  - `config.json`: your token (mode 600). Treat it like a password.
114
134
  - `ssh/id_ed25519`, `ssh/id_ed25519.pub`: your SSH key, created on first connect. The private key never leaves your machine.
115
135
  - `ssh/id_ed25519-cert.pub`: the certificate for the last pod you connected to.
136
+ - `ssh/<pod-id>-cert.pub`, `ssh/config`: the certificate and Host block of each pod set up for VS Code.
116
137
 
117
138
  Delete the folder to reset everything.
118
139
 
@@ -141,23 +162,23 @@ another server or org; the ones you last logged in with are remembered.
141
162
  ## Development
142
163
 
143
164
  The code is in `godfather_cli/`: `cli.py` (commands and menu), `picker.py` (arrow-key menu and pod picker), `auth.py`
144
- (platform sign-in), `pod_manager.py` (API calls), `ssh_connector.py` (keys,
165
+ (platform sign-in), `login_server.py` (the sign-in callback), `pod_manager.py` (API calls), `ssh_connector.py` (keys,
145
166
  certificate, running ssh), `update_checker.py` (PyPI version check), `ui.py`
146
167
  (shared console styling). Tests are in `tests/`:
147
168
 
148
169
  ```bash
149
- pip install -e . pytest
150
- pytest -q tests
170
+ uv sync --group dev
171
+ uv run pytest -q tests
151
172
  ```
152
173
 
174
+ `make check-cli` at the repo root runs ruff, the tests and a build, as CI does.
175
+
153
176
  Releases go to PyPI when a version bump is merged to main; see the
154
177
  repo README.
155
178
 
156
179
  ## Contributing
157
180
 
158
- 1. Fork the repository.
159
- 2. Create a feature branch (`git checkout -b feature/your-feature`).
160
- 3. Commit your changes and open a Pull Request.
181
+ See [CONTRIBUTING.md](../CONTRIBUTING.md).
161
182
 
162
183
  ## License
163
184
 
@@ -33,10 +33,13 @@ pip install -e .
33
33
 
34
34
  1. Run `godfather` with no arguments. On first run you won't be logged in
35
35
  yet, so it'll walk you into the login flow.
36
- 2. It opens the AI Society platform's sign-in page in your browser (and
37
- prints the link). Sign in with Discord and copy the token shown there.
38
- 3. Paste the token back into the terminal. The CLI checks it with the
39
- platform and stores it in `~/.godfather/config.json`.
36
+ 2. It opens the sign-in page in your browser and prints the link. Sign in
37
+ with Discord. The browser sends the login back to the CLI, which checks it
38
+ and stores the token in `~/.godfather/config.json`.
39
+ 3. If the browser cannot reach the CLI (no browser on this computer, an
40
+ older server, or no sign-in within 5 minutes), the CLI asks you to paste
41
+ the token that the sign-in page shows. `godfather auth --paste` goes
42
+ straight to that, for a browser on another computer.
40
43
  4. From the menu (or `godfather connect`), pick a pod. The CLI creates an SSH
41
44
  key on your machine the first time, asks the server for a 12-hour
42
45
  certificate for that pod, and opens the connection.
@@ -52,12 +55,14 @@ Windows 10+ ship it.
52
55
  ### Interactive menu
53
56
 
54
57
  Running `godfather` with no arguments opens a menu. Use the arrow keys to
55
- choose Connect to a pod, List pods, Status, Log out or Exit, and press Enter.
56
- Ctrl-C quits.
58
+ choose Connect to a pod, List pods, Sessions, Set up VS Code, Status, Log out or Exit, and
59
+ press Enter. Ctrl-C quits.
57
60
 
58
61
  Connect to a pod shows a list of your pods with their status, access and
59
62
  pod ID. A pod that is not running shows with the reason and cannot be
60
63
  chosen. If only one pod is running, the CLI asks for a yes instead.
64
+ If none of your pods is running, the CLI shows the next session on your
65
+ pods. A pod starts 10 minutes before its session.
61
66
 
62
67
  When input is not a terminal (for example in a script), the menu and the pod
63
68
  list use numbered prompts.
@@ -68,8 +73,13 @@ list use numbered prompts.
68
73
  godfather list # List pods you can connect to
69
74
  godfather connect # Connect to a pod, picking from a list
70
75
  godfather connect <pod-id> # Connect to a specific pod
76
+ godfather connect --admin # Ask for a root shell (officers, when pod admin is on)
77
+ godfather sessions # Upcoming sessions on your pods, in local time
78
+ godfather vscode # Set up VS Code Remote-SSH for a pod, picking from a list
79
+ godfather vscode <pod-id> # The same for one pod; run it again to renew the certificate
71
80
  godfather status # Show server, org, login, token expiry and version
72
- godfather auth # Log in, or refresh an expired session
81
+ godfather auth # Log in with Discord in your browser
82
+ godfather auth --paste # Log in by pasting the token
73
83
  godfather logout # Clear the stored session
74
84
  godfather update # Update the CLI to the latest version
75
85
 
@@ -77,6 +87,16 @@ godfather update # Update the CLI to the latest version
77
87
  godfather --api-url https://platform.example.com --org soda list
78
88
  ```
79
89
 
90
+ ### VS Code
91
+
92
+ `godfather vscode` sets up a pod for the VS Code Remote - SSH extension:
93
+
94
+ 1. It gets a 12-hour certificate for the pod and saves it as `~/.godfather/ssh/<pod-id>-cert.pub`.
95
+ 2. It writes a `Host godfather-<pod-id>` block to `~/.godfather/ssh/config` and adds one `Include` line for that file at the top of `~/.ssh/config`.
96
+ 3. If the `code` command is on your PATH, it opens VS Code on `/workspace/users/<you>` on the pod. Otherwise run **Remote-SSH: Connect to Host...** in VS Code and pick `godfather-<pod-id>`.
97
+
98
+ VS Code runs as your pod account (`godfather_<you>`), with the same files and limits as `godfather connect`. After 12 hours VS Code cannot reconnect; run `godfather vscode <pod-id>` again. The pod must run image 2.3.0 or later. `godfather vscode --admin` gives officers a root login on a pod with pod admin on.
99
+
80
100
  ## Files it keeps
81
101
 
82
102
  Everything lives in `~/.godfather/`:
@@ -84,6 +104,7 @@ Everything lives in `~/.godfather/`:
84
104
  - `config.json`: your token (mode 600). Treat it like a password.
85
105
  - `ssh/id_ed25519`, `ssh/id_ed25519.pub`: your SSH key, created on first connect. The private key never leaves your machine.
86
106
  - `ssh/id_ed25519-cert.pub`: the certificate for the last pod you connected to.
107
+ - `ssh/<pod-id>-cert.pub`, `ssh/config`: the certificate and Host block of each pod set up for VS Code.
87
108
 
88
109
  Delete the folder to reset everything.
89
110
 
@@ -112,23 +133,23 @@ another server or org; the ones you last logged in with are remembered.
112
133
  ## Development
113
134
 
114
135
  The code is in `godfather_cli/`: `cli.py` (commands and menu), `picker.py` (arrow-key menu and pod picker), `auth.py`
115
- (platform sign-in), `pod_manager.py` (API calls), `ssh_connector.py` (keys,
136
+ (platform sign-in), `login_server.py` (the sign-in callback), `pod_manager.py` (API calls), `ssh_connector.py` (keys,
116
137
  certificate, running ssh), `update_checker.py` (PyPI version check), `ui.py`
117
138
  (shared console styling). Tests are in `tests/`:
118
139
 
119
140
  ```bash
120
- pip install -e . pytest
121
- pytest -q tests
141
+ uv sync --group dev
142
+ uv run pytest -q tests
122
143
  ```
123
144
 
145
+ `make check-cli` at the repo root runs ruff, the tests and a build, as CI does.
146
+
124
147
  Releases go to PyPI when a version bump is merged to main; see the
125
148
  repo README.
126
149
 
127
150
  ## Contributing
128
151
 
129
- 1. Fork the repository.
130
- 2. Create a feature branch (`git checkout -b feature/your-feature`).
131
- 3. Commit your changes and open a Pull Request.
152
+ See [CONTRIBUTING.md](../CONTRIBUTING.md).
132
153
 
133
154
  ## License
134
155
 
@@ -2,6 +2,6 @@
2
2
  Godfather CLI package
3
3
  """
4
4
 
5
- __version__ = "2.1.0"
5
+ __version__ = "2.3.0"
6
6
  __author__ = "AI Society ASU"
7
7
  __email__ = "admin@ais-asu.com"
@@ -1,21 +1,30 @@
1
1
  """CLI authentication: token storage, login flow, and token verification."""
2
2
 
3
+ import base64
3
4
  import datetime
5
+ import hashlib
4
6
  import json
5
7
  import os
6
- import requests
8
+ import secrets
7
9
  import webbrowser
8
10
  from pathlib import Path
9
11
  from typing import Dict, Optional
12
+ from urllib.parse import urlencode
13
+
14
+ import requests
10
15
  from rich.panel import Panel
11
16
  from rich.prompt import Prompt
12
17
 
13
- from .ui import console, success, error, warning, spinner, BOX, BORDER
18
+ from .login_server import LoopbackReceiver
19
+ from .ui import BORDER, BOX, console, error, spinner, success, warning
14
20
 
15
21
  TOKEN_PREFIX = 'plat_'
16
22
  # The platform issues CLI tokens for 90 days.
17
23
  TOKEN_DAYS = 90
18
24
 
25
+ # How long the CLI waits for the browser sign-in.
26
+ LOGIN_WAIT_SECONDS = 300
27
+
19
28
  # Token states that check_token returns.
20
29
  TOKEN_OK = 'ok'
21
30
  TOKEN_MISSING = 'missing'
@@ -24,6 +33,17 @@ TOKEN_UNREACHABLE = 'unreachable'
24
33
  TOKEN_REFUSED = 'refused'
25
34
 
26
35
 
36
+ def make_verifier() -> str:
37
+ """A PKCE code verifier (RFC 7636): 64 URL-safe characters."""
38
+ return secrets.token_urlsafe(48)
39
+
40
+
41
+ def challenge_for(verifier: str) -> str:
42
+ """The S256 code challenge of a verifier: base64url SHA-256 without padding."""
43
+ digest = hashlib.sha256(verifier.encode('ascii')).digest()
44
+ return base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
45
+
46
+
27
47
  class CLIAuthenticator:
28
48
  """Handle CLI authentication."""
29
49
 
@@ -61,8 +81,84 @@ class CLIAuthenticator:
61
81
  """URL of a platform compute route for the configured org."""
62
82
  return f"{self.api_base}/api/compute/{self.org}{path}"
63
83
 
64
- def authenticate(self) -> bool:
65
- """Walk the user through signing in with Discord and pasting the token it shows."""
84
+ def authenticate(self, paste: bool = False) -> bool:
85
+ """Sign in with Discord. The browser hands the token back; the paste flow is the fallback."""
86
+ if not paste:
87
+ if self.supports_loopback():
88
+ token = self._browser_login()
89
+ if token:
90
+ return self._verify_and_save(token)
91
+ else:
92
+ console.print("[dim]This server does not support browser sign-in yet. Paste the token instead.[/dim]")
93
+ return self._paste_login()
94
+
95
+ def supports_loopback(self) -> bool:
96
+ """True when the server has the cli/token route. It answers 400 to an empty body."""
97
+ try:
98
+ response = requests.post(self.compute_url('/cli/token'), json={}, timeout=10)
99
+ except requests.RequestException:
100
+ return False
101
+ return response.status_code == 400
102
+
103
+ def _browser_login(self) -> Optional[str]:
104
+ """The token from the localhost callback with PKCE, or None after printing why not."""
105
+ state = secrets.token_urlsafe(16)
106
+ verifier = make_verifier()
107
+ try:
108
+ receiver = LoopbackReceiver(state)
109
+ except OSError:
110
+ console.print("[dim]Could not listen on 127.0.0.1. Paste the token instead.[/dim]")
111
+ return None
112
+ try:
113
+ query = urlencode(
114
+ {
115
+ 'port': receiver.port,
116
+ 'state': state,
117
+ 'code_challenge': challenge_for(verifier),
118
+ 'code_challenge_method': 'S256',
119
+ }
120
+ )
121
+ login_url = f"{self.compute_url('/cli/login')}?{query}"
122
+ try:
123
+ opened = webbrowser.open(login_url)
124
+ except webbrowser.Error:
125
+ opened = False
126
+ if not opened:
127
+ console.print("[dim]No browser opened on this computer. Paste the token instead.[/dim]")
128
+ return None
129
+ console.print(f"Sign in with Discord in your browser. If it did not open, go to:\n{login_url}")
130
+ try:
131
+ with spinner("Waiting for the browser sign-in..."):
132
+ code = receiver.wait(LOGIN_WAIT_SECONDS)
133
+ except KeyboardInterrupt:
134
+ console.print("[dim]Cancelled[/dim]")
135
+ return None
136
+ finally:
137
+ receiver.close()
138
+ if not code:
139
+ console.print("[dim]The browser sign-in did not finish in time. Paste the token instead.[/dim]")
140
+ return None
141
+ return self._redeem(code, verifier)
142
+
143
+ def _redeem(self, code: str, verifier: str) -> Optional[str]:
144
+ """Exchange the one-time code and the PKCE verifier for a token."""
145
+ try:
146
+ with spinner("Getting your token..."):
147
+ response = requests.post(
148
+ self.compute_url('/cli/token'),
149
+ json={'code': code, 'code_verifier': verifier},
150
+ timeout=10,
151
+ )
152
+ token = str(response.json().get('token') or '') if response.status_code == 200 else ''
153
+ except (requests.RequestException, ValueError, AttributeError):
154
+ token = ''
155
+ if not token.startswith(TOKEN_PREFIX):
156
+ console.print("[dim]The server did not return a token. Paste the token instead.[/dim]")
157
+ return None
158
+ return token
159
+
160
+ def _paste_login(self) -> bool:
161
+ """Open the sign-in page and take the token the member pastes."""
66
162
  login_url = self.compute_url('/cli/login')
67
163
  panel = Panel(
68
164
  f"[bold]1.[/bold] Open [bold]{login_url}[/bold] and sign in with Discord\n"
@@ -91,13 +187,14 @@ class CLIAuthenticator:
91
187
  error("That doesn't look like a valid Godfather token")
92
188
  console.print(f"[dim]Tokens start with {TOKEN_PREFIX}. Get a fresh one from {login_url}[/dim]")
93
189
  return False
190
+ return self._verify_and_save(token)
94
191
 
192
+ def _verify_and_save(self, token: str) -> bool:
193
+ """Check the token with GET /me/pods, then save it with the server and org."""
95
194
  try:
96
195
  with spinner("Verifying token..."):
97
196
  response = requests.get(
98
- self.compute_url('/me/pods'),
99
- headers={'Authorization': f'Bearer {token}'},
100
- timeout=10
197
+ self.compute_url('/me/pods'), headers={'Authorization': f'Bearer {token}'}, timeout=10
101
198
  )
102
199
  except requests.ConnectionError:
103
200
  error(f"Couldn't reach {self.api_base}")
@@ -153,11 +250,7 @@ class CLIAuthenticator:
153
250
  return TOKEN_MISSING
154
251
 
155
252
  try:
156
- response = requests.get(
157
- self.compute_url('/me/pods'),
158
- headers=self.auth_headers(),
159
- timeout=5
160
- )
253
+ response = requests.get(self.compute_url('/me/pods'), headers=self.auth_headers(), timeout=5)
161
254
  except requests.RequestException:
162
255
  return TOKEN_UNREACHABLE
163
256
  if response.status_code == 200: