softmax-cli 0.26.13__tar.gz → 0.26.15__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.
- softmax_cli-0.26.15/AGENTS.md +48 -0
- softmax_cli-0.26.15/BUILD.bazel +3 -0
- softmax_cli-0.26.15/CLAUDE.md +48 -0
- softmax_cli-0.26.15/PKG-INFO +54 -0
- softmax_cli-0.26.15/README.md +35 -0
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/pyproject.toml +5 -4
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax/__init__.py +5 -13
- softmax_cli-0.26.15/src/softmax/auth.py +166 -0
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax/cli.py +35 -38
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax/perform_login.py +28 -49
- softmax_cli-0.26.15/src/softmax_cli.egg-info/PKG-INFO +54 -0
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax_cli.egg-info/SOURCES.txt +3 -2
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax_cli.egg-info/requires.txt +6 -5
- softmax_cli-0.26.15/tests/BUILD.bazel +3 -0
- softmax_cli-0.26.15/tests/test_auth_login.py +228 -0
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/tests/test_python_api.py +16 -17
- softmax_cli-0.26.13/BUILD.bazel +0 -19
- softmax_cli-0.26.13/PKG-INFO +0 -17
- softmax_cli-0.26.13/src/softmax/auth.py +0 -100
- softmax_cli-0.26.13/src/softmax/token_storage.py +0 -102
- softmax_cli-0.26.13/src/softmax_cli.egg-info/PKG-INFO +0 -17
- softmax_cli-0.26.13/tests/BUILD.bazel +0 -38
- softmax_cli-0.26.13/tests/test_auth_login.py +0 -344
- softmax_cli-0.26.13/tests/test_cli_plugins.py +0 -24
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/setup.cfg +0 -0
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax/_console.py +0 -0
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax/cogames.py +0 -0
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax_cli.egg-info/dependency_links.txt +0 -0
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax_cli.egg-info/entry_points.txt +0 -0
- {softmax_cli-0.26.13 → softmax_cli-0.26.15}/src/softmax_cli.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# AGENTS.md — softmax-cli
|
|
2
|
+
|
|
3
|
+
The `softmax` CLI: authentication and account tools for Softmax/Observatory. Other packages (e.g. `coworld`) depend on
|
|
4
|
+
it for login/token handling and pin an exact version. Deps: `typer`, `rich`, `httpx`, plus `fastapi`/`uvicorn` for the
|
|
5
|
+
local browser-login callback server. The `cogames` optional extra mounts the cogames CLI as a `softmax cogames`
|
|
6
|
+
subcommand. Published to PyPI.
|
|
7
|
+
|
|
8
|
+
## CLI
|
|
9
|
+
|
|
10
|
+
Installs a `softmax` entrypoint (`softmax.cli:app`, Typer):
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv run softmax login # browser-based login (spins up a local callback server)
|
|
14
|
+
uv run softmax logout
|
|
15
|
+
uv run softmax status
|
|
16
|
+
uv run softmax get-login-url
|
|
17
|
+
uv run softmax get-token / set-token
|
|
18
|
+
uv run softmax cogames ... # only when installed with the `cogames` extra
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Tests
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv run metta pytest packages/softmax-cli/tests -v
|
|
25
|
+
uv run metta pytest --changed
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Tests cover auth/login, the Python API, and CLI plugin wiring; a `BUILD.bazel` exists under `tests/`.
|
|
29
|
+
|
|
30
|
+
## Lint
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv run metta lint --fix # ruff (also runs via the Edit/Write hook)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Source layout (`src/softmax/`)
|
|
37
|
+
|
|
38
|
+
- `cli.py` — the Typer app; mounts the optional `cogames` subapp via `add_typer`.
|
|
39
|
+
- `auth.py` — token storage, browser login URL, and `whoami` HTTP helpers.
|
|
40
|
+
- `perform_login.py` — the local FastAPI/uvicorn callback server used during `softmax login`.
|
|
41
|
+
- `cogames.py` — the optional cogames subcommand wiring.
|
|
42
|
+
- `_console.py` — shared rich console helpers.
|
|
43
|
+
|
|
44
|
+
## Gotchas
|
|
45
|
+
|
|
46
|
+
- Versioned via `setuptools_scm` off `softmax-v*` git tags (`fallback_version = 0.0.0`).
|
|
47
|
+
- Downstream packages pin an exact `softmax-cli==X.Y.Z`; bumping the public auth API can break them — coordinate version
|
|
48
|
+
bumps with consumers like `coworld`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# AGENTS.md — softmax-cli
|
|
2
|
+
|
|
3
|
+
The `softmax` CLI: authentication and account tools for Softmax/Observatory. Other packages (e.g. `coworld`) depend on
|
|
4
|
+
it for login/token handling and pin an exact version. Deps: `typer`, `rich`, `httpx`, plus `fastapi`/`uvicorn` for the
|
|
5
|
+
local browser-login callback server. The `cogames` optional extra mounts the cogames CLI as a `softmax cogames`
|
|
6
|
+
subcommand. Published to PyPI.
|
|
7
|
+
|
|
8
|
+
## CLI
|
|
9
|
+
|
|
10
|
+
Installs a `softmax` entrypoint (`softmax.cli:app`, Typer):
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv run softmax login # browser-based login (spins up a local callback server)
|
|
14
|
+
uv run softmax logout
|
|
15
|
+
uv run softmax status
|
|
16
|
+
uv run softmax get-login-url
|
|
17
|
+
uv run softmax get-token / set-token
|
|
18
|
+
uv run softmax cogames ... # only when installed with the `cogames` extra
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Tests
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv run metta pytest packages/softmax-cli/tests -v
|
|
25
|
+
uv run metta pytest --changed
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Tests cover auth/login, the Python API, and CLI plugin wiring; a `BUILD.bazel` exists under `tests/`.
|
|
29
|
+
|
|
30
|
+
## Lint
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv run metta lint --fix # ruff (also runs via the Edit/Write hook)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Source layout (`src/softmax/`)
|
|
37
|
+
|
|
38
|
+
- `cli.py` — the Typer app; mounts the optional `cogames` subapp via `add_typer`.
|
|
39
|
+
- `auth.py` — token storage, browser login URL, and `whoami` HTTP helpers.
|
|
40
|
+
- `perform_login.py` — the local FastAPI/uvicorn callback server used during `softmax login`.
|
|
41
|
+
- `cogames.py` — the optional cogames subcommand wiring.
|
|
42
|
+
- `_console.py` — shared rich console helpers.
|
|
43
|
+
|
|
44
|
+
## Gotchas
|
|
45
|
+
|
|
46
|
+
- Versioned via `setuptools_scm` off `softmax-v*` git tags (`fallback_version = 0.0.0`).
|
|
47
|
+
- Downstream packages pin an exact `softmax-cli==X.Y.Z`; bumping the public auth API can break them — coordinate version
|
|
48
|
+
bumps with consumers like `coworld`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: softmax-cli
|
|
3
|
+
Version: 0.26.15
|
|
4
|
+
Summary: Softmax CLI — authentication and account tools
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Requires-Python: <3.13,>=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: fastapi>=0.115.0
|
|
11
|
+
Requires-Dist: httpx>=0.28.1
|
|
12
|
+
Requires-Dist: pydantic>=2.0
|
|
13
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
14
|
+
Requires-Dist: rich>=13.7.0
|
|
15
|
+
Requires-Dist: typer>=0.19.2
|
|
16
|
+
Requires-Dist: uvicorn>=0.34.0
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
19
|
+
|
|
20
|
+
# softmax-cli
|
|
21
|
+
|
|
22
|
+
The `softmax` command-line tool: authentication and account management for Softmax / Observatory. It provides
|
|
23
|
+
browser-based login (with a local callback server), token storage, and account status. Other packages — notably
|
|
24
|
+
`coworld` — depend on it for auth. Installing with the `cogames` extra mounts the cogames CLI as a `softmax cogames`
|
|
25
|
+
subcommand.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install softmax-cli # standalone
|
|
31
|
+
uv tool install "softmax-cli[cogames]" # with the cogames subcommand
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Within the metta workspace it is available via `uv sync`.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv run softmax login # log in via the browser
|
|
40
|
+
uv run softmax status # show current auth status
|
|
41
|
+
uv run softmax logout
|
|
42
|
+
uv run softmax get-token # print the stored token
|
|
43
|
+
uv run softmax set-token # store a token manually
|
|
44
|
+
uv run softmax cogames ... # only with the `cogames` extra
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Development
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv run metta pytest packages/softmax-cli/tests -v # run tests
|
|
51
|
+
uv run metta lint --fix # lint/format
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
See [AGENTS.md](AGENTS.md) for the source layout and versioning/compatibility notes.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# softmax-cli
|
|
2
|
+
|
|
3
|
+
The `softmax` command-line tool: authentication and account management for Softmax / Observatory. It provides
|
|
4
|
+
browser-based login (with a local callback server), token storage, and account status. Other packages — notably
|
|
5
|
+
`coworld` — depend on it for auth. Installing with the `cogames` extra mounts the cogames CLI as a `softmax cogames`
|
|
6
|
+
subcommand.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv tool install softmax-cli # standalone
|
|
12
|
+
uv tool install "softmax-cli[cogames]" # with the cogames subcommand
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Within the metta workspace it is available via `uv sync`.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv run softmax login # log in via the browser
|
|
21
|
+
uv run softmax status # show current auth status
|
|
22
|
+
uv run softmax logout
|
|
23
|
+
uv run softmax get-token # print the stored token
|
|
24
|
+
uv run softmax set-token # store a token manually
|
|
25
|
+
uv run softmax cogames ... # only with the `cogames` extra
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv run metta pytest packages/softmax-cli/tests -v # run tests
|
|
32
|
+
uv run metta lint --fix # lint/format
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
See [AGENTS.md](AGENTS.md) for the source layout and versioning/compatibility notes.
|
|
@@ -13,17 +13,18 @@ classifiers = [
|
|
|
13
13
|
"Programming Language :: Python :: 3.12",
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"typer>=0.19.2",
|
|
17
|
-
"rich>=13.7.0",
|
|
18
16
|
"fastapi>=0.115.0",
|
|
19
|
-
"uvicorn>=0.34.0",
|
|
20
17
|
"httpx>=0.28.1",
|
|
18
|
+
"pydantic>=2.0",
|
|
21
19
|
"pyyaml>=6.0.2",
|
|
20
|
+
"rich>=13.7.0",
|
|
21
|
+
"typer>=0.19.2",
|
|
22
|
+
"uvicorn>=0.34.0",
|
|
22
23
|
]
|
|
23
24
|
dynamic = ["version"]
|
|
24
25
|
|
|
25
26
|
[project.optional-dependencies]
|
|
26
|
-
|
|
27
|
+
test = ["pytest>=8.0"]
|
|
27
28
|
|
|
28
29
|
[project.scripts]
|
|
29
30
|
softmax = "softmax.cli:app"
|
|
@@ -4,13 +4,8 @@ import importlib
|
|
|
4
4
|
import sys
|
|
5
5
|
from pkgutil import extend_path
|
|
6
6
|
|
|
7
|
-
from softmax.auth import
|
|
8
|
-
DEFAULT_API_SERVER,
|
|
9
|
-
load_cogames_user_token,
|
|
10
|
-
save_cogames_active_token,
|
|
11
|
-
)
|
|
7
|
+
from softmax.auth import DEFAULT_API_SERVER, load_user_token
|
|
12
8
|
from softmax.perform_login import do_interactive_login_for_token
|
|
13
|
-
from softmax.token_storage import TokenKind
|
|
14
9
|
|
|
15
10
|
__path__ = extend_path(__path__, __name__)
|
|
16
11
|
|
|
@@ -21,9 +16,8 @@ def login(
|
|
|
21
16
|
force: bool = False,
|
|
22
17
|
open_browser: bool = True,
|
|
23
18
|
) -> str:
|
|
24
|
-
token = None if force else
|
|
19
|
+
token = None if force else load_user_token(server=api_server)
|
|
25
20
|
if token is not None:
|
|
26
|
-
save_cogames_active_token(api_server=api_server, token=token)
|
|
27
21
|
return token
|
|
28
22
|
|
|
29
23
|
if not sys.stdin.isatty():
|
|
@@ -34,20 +28,18 @@ def login(
|
|
|
34
28
|
|
|
35
29
|
do_interactive_login_for_token(
|
|
36
30
|
api_server=api_server,
|
|
37
|
-
token_kind=TokenKind.COGAMES_USER,
|
|
38
31
|
agent_hint=(
|
|
39
32
|
"If you are a coding agent, ask your human to open the URL below and give you "
|
|
40
|
-
"the auth
|
|
33
|
+
"the auth code. Then paste the code into this window or run:\n"
|
|
41
34
|
"\n"
|
|
42
|
-
"softmax
|
|
35
|
+
"softmax exchange-code '<CODE>'"
|
|
43
36
|
),
|
|
44
37
|
open_browser=open_browser,
|
|
45
38
|
)
|
|
46
39
|
|
|
47
|
-
token =
|
|
40
|
+
token = load_user_token(server=api_server)
|
|
48
41
|
if token is None:
|
|
49
42
|
raise RuntimeError(f"Interactive login did not save a token for {api_server}")
|
|
50
|
-
save_cogames_active_token(api_server=api_server, token=token)
|
|
51
43
|
return token
|
|
52
44
|
|
|
53
45
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Auth helpers for the softmax CLI: token storage, browser login URL, whoami."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import yaml
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
DEFAULT_API_SERVER = "https://softmax.com/api"
|
|
14
|
+
|
|
15
|
+
CREDENTIALS_FILE = "credentials.yaml"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_api_server() -> str:
|
|
19
|
+
return os.environ.get("COGAMES_API_URL", DEFAULT_API_SERVER)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --- on-disk storage ---
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _config_dir() -> Path:
|
|
26
|
+
config_dir = Path.home() / ".softmax"
|
|
27
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
return config_dir
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _credentials_path() -> Path:
|
|
32
|
+
return _config_dir() / CREDENTIALS_FILE
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load_data() -> dict:
|
|
36
|
+
cred_path = _credentials_path()
|
|
37
|
+
if not cred_path.exists():
|
|
38
|
+
return {"tokens": {}, "player_sessions": {}}
|
|
39
|
+
|
|
40
|
+
with open(cred_path, "r") as f:
|
|
41
|
+
return yaml.safe_load(f) or {"tokens": {}, "player_sessions": {}}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _save_data(data: dict) -> None:
|
|
45
|
+
cred_path = _credentials_path()
|
|
46
|
+
with open(cred_path, "w") as f:
|
|
47
|
+
yaml.safe_dump(data, f, default_flow_style=False)
|
|
48
|
+
os.chmod(cred_path, 0o600)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# --- public token API ---
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_user_token(*, server: str) -> str | None:
|
|
55
|
+
server = server.rstrip("/")
|
|
56
|
+
tokens = _load_data().get("tokens", {})
|
|
57
|
+
token = tokens.get(server)
|
|
58
|
+
return token if isinstance(token, str) else None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def save_user_token(*, server: str, token: str) -> None:
|
|
62
|
+
server = server.rstrip("/")
|
|
63
|
+
data = _load_data()
|
|
64
|
+
data.setdefault("tokens", {})[server] = token
|
|
65
|
+
_save_data(data)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _delete_user_token(*, server: str) -> bool:
|
|
69
|
+
server = server.rstrip("/")
|
|
70
|
+
data = _load_data()
|
|
71
|
+
tokens = data.get("tokens", {})
|
|
72
|
+
if server not in tokens:
|
|
73
|
+
return False
|
|
74
|
+
del tokens[server]
|
|
75
|
+
_save_data(data)
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_player_session(*, server: str) -> str | None:
|
|
80
|
+
server = server.rstrip("/")
|
|
81
|
+
sessions = _load_data().get("player_sessions", {})
|
|
82
|
+
token = sessions.get(server)
|
|
83
|
+
return token if isinstance(token, str) else None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def save_player_session(*, server: str, token: str) -> None:
|
|
87
|
+
server = server.rstrip("/")
|
|
88
|
+
data = _load_data()
|
|
89
|
+
data.setdefault("player_sessions", {})[server] = token
|
|
90
|
+
_save_data(data)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _delete_player_session(*, server: str) -> bool:
|
|
94
|
+
server = server.rstrip("/")
|
|
95
|
+
data = _load_data()
|
|
96
|
+
sessions = data.get("player_sessions", {})
|
|
97
|
+
if server not in sessions:
|
|
98
|
+
return False
|
|
99
|
+
del sessions[server]
|
|
100
|
+
_save_data(data)
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_current_token(*, server: str) -> str | None:
|
|
105
|
+
"""Returns player_session if present, else user token."""
|
|
106
|
+
return load_player_session(server=server) or load_user_token(server=server)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def has_saved_token(*, server: str) -> bool:
|
|
110
|
+
return load_current_token(server=server) is not None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def delete_all_tokens(*, server: str) -> bool:
|
|
114
|
+
deleted_token = _delete_user_token(server=server)
|
|
115
|
+
deleted_session = _delete_player_session(server=server)
|
|
116
|
+
return deleted_token or deleted_session
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- HTTP / URL helpers ---
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class WhoAmIResponse(BaseModel):
|
|
123
|
+
user_email: str
|
|
124
|
+
is_softmax_team_member: bool = False
|
|
125
|
+
is_softmax_admin: bool = False
|
|
126
|
+
subject_type: str = "user"
|
|
127
|
+
subject_id: str | None = None
|
|
128
|
+
owner_user_id: str | None = None
|
|
129
|
+
scopes: list[str] = Field(default_factory=list)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_browser_login_url(api_server: str, *, callback_url: str | None = None) -> str:
|
|
133
|
+
"""Build the hosted browser sign-in URL for CLI login."""
|
|
134
|
+
params: dict[str, str] = {}
|
|
135
|
+
if callback_url:
|
|
136
|
+
params["callback"] = callback_url
|
|
137
|
+
|
|
138
|
+
query = urlencode(params)
|
|
139
|
+
parsed = urlsplit(api_server)
|
|
140
|
+
# Code-exchange flow lives at /cli-auth; the legacy /cli-login route is
|
|
141
|
+
# preserved untouched for published softmax-cli <= 0.26.14 (pinned by
|
|
142
|
+
# coworld[auth]) until those releases age out.
|
|
143
|
+
browser_path = parsed.path.rstrip("/").removesuffix("/api") + "/cli-auth"
|
|
144
|
+
return urlunsplit((parsed.scheme, parsed.netloc, browser_path, query, ""))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def fetch_cogames_whoami(*, api_server: str | None = None, token: str) -> WhoAmIResponse:
|
|
148
|
+
server = api_server or DEFAULT_API_SERVER
|
|
149
|
+
response = httpx.get(
|
|
150
|
+
f"{server.rstrip('/')}/observatory/whoami",
|
|
151
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
152
|
+
timeout=10.0,
|
|
153
|
+
)
|
|
154
|
+
response.raise_for_status()
|
|
155
|
+
return WhoAmIResponse.model_validate(response.json())
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def exchange_auth_code(*, api_server: str, code: str) -> str:
|
|
159
|
+
"""Exchange a one-time auth code for a real credential token."""
|
|
160
|
+
response = httpx.post(
|
|
161
|
+
f"{api_server.rstrip('/')}/observatory/credentials/auth-codes/exchange",
|
|
162
|
+
json={"code": code},
|
|
163
|
+
timeout=10.0,
|
|
164
|
+
)
|
|
165
|
+
response.raise_for_status()
|
|
166
|
+
return response.json()["token"]
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""softmax CLI — authentication and account tools."""
|
|
2
2
|
|
|
3
|
-
import importlib
|
|
4
|
-
import importlib.util
|
|
5
3
|
import sys
|
|
6
4
|
|
|
7
5
|
import httpx
|
|
@@ -12,16 +10,15 @@ from softmax._console import console
|
|
|
12
10
|
from softmax.auth import (
|
|
13
11
|
DEFAULT_API_SERVER,
|
|
14
12
|
build_browser_login_url,
|
|
15
|
-
|
|
13
|
+
delete_all_tokens,
|
|
14
|
+
exchange_auth_code,
|
|
16
15
|
fetch_cogames_whoami,
|
|
17
16
|
get_api_server,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
save_cogames_user_token,
|
|
17
|
+
load_current_token,
|
|
18
|
+
load_user_token,
|
|
19
|
+
save_user_token,
|
|
22
20
|
)
|
|
23
21
|
from softmax.perform_login import do_interactive_login_for_token
|
|
24
|
-
from softmax.token_storage import TokenKind
|
|
25
22
|
|
|
26
23
|
app = typer.Typer(
|
|
27
24
|
help="Softmax CLI — authentication and account tools",
|
|
@@ -31,20 +28,10 @@ app = typer.Typer(
|
|
|
31
28
|
)
|
|
32
29
|
|
|
33
30
|
|
|
34
|
-
def
|
|
35
|
-
if importlib.util.find_spec("cogames") is None:
|
|
36
|
-
return
|
|
37
|
-
if importlib.util.find_spec("cogames.softmax_cli") is None:
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
cogames_cli = importlib.import_module("cogames.softmax_cli")
|
|
41
|
-
app.add_typer(cogames_cli.app, name="cogames", rich_help_panel="Local Games")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _build_manual_set_token_command(server: str | None = None) -> str:
|
|
31
|
+
def _build_manual_exchange_command(server: str | None = None) -> str:
|
|
45
32
|
if server:
|
|
46
|
-
return f"softmax
|
|
47
|
-
return "softmax
|
|
33
|
+
return f"softmax exchange-code --server '{server}' '<CODE>'"
|
|
34
|
+
return "softmax exchange-code '<CODE>'"
|
|
48
35
|
|
|
49
36
|
|
|
50
37
|
def _print_non_tty_login_instructions(api_server: str | None = None) -> None:
|
|
@@ -57,14 +44,14 @@ def _print_non_tty_login_instructions(api_server: str | None = None) -> None:
|
|
|
57
44
|
console.print()
|
|
58
45
|
console.print(" ", auth_url)
|
|
59
46
|
console.print()
|
|
60
|
-
console.print("Copy the auth
|
|
47
|
+
console.print("Copy the auth code from the browser, then run:", style="yellow")
|
|
61
48
|
console.print()
|
|
62
|
-
console.print(" ",
|
|
49
|
+
console.print(" ", _build_manual_exchange_command(resolved if is_custom else None))
|
|
63
50
|
console.print()
|
|
64
51
|
console.print(
|
|
65
52
|
Panel(
|
|
66
|
-
"If you are a coding agent, ask your human to open the URL above and give you
|
|
67
|
-
"Then run the
|
|
53
|
+
"If you are a coding agent, ask your human to open the URL above and give you "
|
|
54
|
+
"the auth code. Then run the exchange-code command above.",
|
|
68
55
|
title="🤖 Agent Hint",
|
|
69
56
|
border_style="cyan",
|
|
70
57
|
)
|
|
@@ -96,7 +83,7 @@ def login_cmd(
|
|
|
96
83
|
from urllib.parse import urlparse # noqa: PLC0415
|
|
97
84
|
|
|
98
85
|
api_server = server or get_api_server()
|
|
99
|
-
user_token = None if force else
|
|
86
|
+
user_token = None if force else load_user_token(server=api_server)
|
|
100
87
|
if user_token is not None:
|
|
101
88
|
try:
|
|
102
89
|
whoami = fetch_cogames_whoami(api_server=api_server, token=user_token)
|
|
@@ -113,7 +100,6 @@ def login_cmd(
|
|
|
113
100
|
console.print("Could not reach server to verify token. Proceeding with saved token.", style="yellow")
|
|
114
101
|
|
|
115
102
|
if user_token is not None:
|
|
116
|
-
save_cogames_active_token(api_server=api_server, token=user_token)
|
|
117
103
|
console.print(f"Already authenticated with {urlparse(api_server).hostname}", style="green")
|
|
118
104
|
return
|
|
119
105
|
|
|
@@ -124,12 +110,11 @@ def login_cmd(
|
|
|
124
110
|
try:
|
|
125
111
|
do_interactive_login_for_token(
|
|
126
112
|
api_server=api_server,
|
|
127
|
-
token_kind=TokenKind.COGAMES_USER,
|
|
128
113
|
agent_hint=(
|
|
129
114
|
"If you are a coding agent, ask your human to open the URL below and give you "
|
|
130
|
-
"the auth
|
|
115
|
+
"the auth code. Then paste the code into this window or run:\n"
|
|
131
116
|
"\n"
|
|
132
|
-
f"{
|
|
117
|
+
f"{_build_manual_exchange_command(server)}"
|
|
133
118
|
),
|
|
134
119
|
open_browser=not no_browser,
|
|
135
120
|
)
|
|
@@ -139,9 +124,6 @@ def login_cmd(
|
|
|
139
124
|
console.print("Authentication failed.", style="red")
|
|
140
125
|
raise typer.Exit(1) from e
|
|
141
126
|
|
|
142
|
-
user_token = load_cogames_user_token(api_server=api_server)
|
|
143
|
-
assert user_token is not None
|
|
144
|
-
save_cogames_active_token(api_server=api_server, token=user_token)
|
|
145
127
|
console.print("Authentication successful.", style="green")
|
|
146
128
|
|
|
147
129
|
|
|
@@ -157,7 +139,7 @@ def logout_cmd(
|
|
|
157
139
|
) -> None:
|
|
158
140
|
"""Remove saved authentication token."""
|
|
159
141
|
api_server = server or get_api_server()
|
|
160
|
-
if
|
|
142
|
+
if delete_all_tokens(server=api_server):
|
|
161
143
|
console.print("Logged out.", style="green")
|
|
162
144
|
else:
|
|
163
145
|
console.print("No token found — already logged out.", style="yellow")
|
|
@@ -189,7 +171,7 @@ def status_cmd(
|
|
|
189
171
|
) -> None:
|
|
190
172
|
"""Check authentication status via /whoami."""
|
|
191
173
|
api_server = server or get_api_server()
|
|
192
|
-
token =
|
|
174
|
+
token = load_current_token(server=api_server)
|
|
193
175
|
if not token:
|
|
194
176
|
console.print("[red]Not authenticated.[/red] Run [cyan]softmax login[/cyan] first.")
|
|
195
177
|
raise typer.Exit(1)
|
|
@@ -217,7 +199,7 @@ def get_token_cmd(
|
|
|
217
199
|
) -> None:
|
|
218
200
|
"""Print the saved token to stdout (for scripting)."""
|
|
219
201
|
api_server = server or get_api_server()
|
|
220
|
-
token =
|
|
202
|
+
token = load_current_token(server=api_server)
|
|
221
203
|
if not token:
|
|
222
204
|
console.print("[red]No token found.[/red] Run [cyan]softmax login[/cyan] first.", style="bold")
|
|
223
205
|
raise typer.Exit(1)
|
|
@@ -237,8 +219,23 @@ def set_token_cmd(
|
|
|
237
219
|
) -> None:
|
|
238
220
|
"""Manually set a token (for CI or headless environments)."""
|
|
239
221
|
api_server = server or get_api_server()
|
|
240
|
-
|
|
222
|
+
save_user_token(server=api_server, token=token)
|
|
241
223
|
print(f"\nToken saved for {api_server}")
|
|
242
224
|
|
|
243
225
|
|
|
244
|
-
|
|
226
|
+
@app.command(name="exchange-code")
|
|
227
|
+
def exchange_code_cmd(
|
|
228
|
+
code: str = typer.Argument(help="One-time auth code from browser login"),
|
|
229
|
+
server: str | None = typer.Option(
|
|
230
|
+
None,
|
|
231
|
+
"--server",
|
|
232
|
+
"-s",
|
|
233
|
+
metavar="URL",
|
|
234
|
+
help="API server URL.",
|
|
235
|
+
),
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Exchange a one-time auth code for a credential."""
|
|
238
|
+
api_server = server or get_api_server()
|
|
239
|
+
token = exchange_auth_code(api_server=api_server, code=code)
|
|
240
|
+
save_user_token(server=api_server, token=token)
|
|
241
|
+
print(f"\nToken saved for {api_server}")
|