Mimiry 0.2.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.
mimiry-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: Mimiry
3
+ Version: 0.2.1
4
+ Summary: Python SDK for serverless compute
5
+ Author-email: Oliver <oliversorensen39@gmail.com>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://mimiry.com
8
+ Project-URL: Repository, https://github.com/OTSorensen/mimiry-python-sdk
9
+ Project-URL: Issues, https://github.com/OTSorensen/mimiry-python-sdk/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: cloudpickle>=3.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8; extra == "dev"
20
+ Requires-Dist: ruff>=0.5; extra == "dev"
21
+
22
+ # mimiry — Python SDK for Mimiry GPU compute
23
+
24
+ **Status:** alpha — early access
25
+ **Backend:** softlaunch.mimiry.com (beta)
26
+
27
+ Python-native interface for running serverless cloud GPU jobs on Mimiry, with full control over locality and providers.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install mimiry
33
+ ```
34
+
35
+ Or, for local development from a clone of this repo (editable install):
36
+
37
+ ```bash
38
+ pip install -e .
39
+ ```
40
+
41
+ ## Auth
42
+
43
+ Running jobs requires a Mimiry account. The SDK authenticates with SSH-JWT — the same SSH key you register on your account at the [Mimiry portal](https://softlaunch.mimiry.com). The fastest way to get set up is the interactive wizard, which generates a key (if needed), walks you through registering it in the portal, writes `MIMIRY_SSH_KEY` to your shell profile, and verifies the connection:
44
+
45
+ ```bash
46
+ mimiry setup # alias: mimiry init
47
+ ```
48
+
49
+ This is a one-time step — you're set going forward.
50
+
51
+ To configure auth manually instead, point the SDK at your private key:
52
+
53
+ ```bash
54
+ export MIMIRY_SSH_KEY=~/.ssh/mimiry
55
+ ```
56
+
57
+ Or pass `ssh_key_path=` explicitly to `mimiry.configure()`.
58
+
59
+ ## GPU types and providers
60
+
61
+ Mimiry sources GPUs from both local datacenters and cloud providers across Europe and the US, spanning entry-level cards up to the latest high-end accelerators. You control locality and hardware requirements, as well as which providers to use.
62
+
63
+ Always check what's currently available before selecting hardware:
64
+
65
+ ```bash
66
+ mimiry availability
67
+ ```
68
+
69
+ To filter to a single GPU family, add `--gpu-family <FAMILY>`.
70
+
71
+ ## Python version
72
+
73
+ Your local Python `major.minor` must match the Python inside your container
74
+ image. The SDK ships your function to the GPU with `cloudpickle`, which can't
75
+ move code objects across Python versions — e.g. a function pickled on 3.12
76
+ won't load on 3.10.
77
+
78
+ You don't need to think about this with the default image: it ships Python
79
+ 3.12, matching recent Ubuntu / Debian / Fedora. It only matters if you set
80
+ `image=` yourself — pick one whose `python3` matches your local interpreter.
81
+ Confirm with `python3 --version` locally and inside the image; a mismatch
82
+ shows up as a failure to deserialize your function.
83
+
84
+ ## Quickstart — one-shot function
85
+
86
+ ```python
87
+ import mimiry
88
+
89
+ @mimiry.function(
90
+ # Uses default hardware; run `mimiry availability` to choose a GPU/provider.
91
+ image="nvcr.io/nvidia/cuda:12.6.2-runtime-ubuntu24.04",
92
+ )
93
+ def gpu_info():
94
+ import subprocess
95
+ return subprocess.check_output(
96
+ ["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv"],
97
+ text=True,
98
+ )
99
+
100
+ print(gpu_info.remote())
101
+ ```
102
+
103
+ ## Quickstart — raw bash command
104
+
105
+ ```python
106
+ import mimiry
107
+
108
+ result = mimiry.run(
109
+ image="nvcr.io/nvidia/cuda:12.6.2-runtime-ubuntu24.04",
110
+ command="nvidia-smi",
111
+ )
112
+ print(result.logs)
113
+ ```
114
+
115
+ ## What works in this version
116
+
117
+ - `@mimiry.function(gpu=..., image=...)` decorator
118
+ - `.remote(*args, **kwargs)` — sync call, returns the function's return value
119
+ - `.map(iterable)` — runs the function over an iterable, sequentially
120
+ - `Image.from_registry(uri).pip_install(...).apt_install(...)` — basic image customisation (installs at container start; no real Dockerfile build)
121
+ - `mimiry.run(image, gpu, command)` — raw bash entrypoint
122
+ - SSH-JWT auth via existing key
123
+
124
+ ## Examples
125
+
126
+ See `examples/`:
127
+
128
+ - `01_hello.py` — minimal `nvidia-smi` on a GPU
129
+ - `02_cuda_probe.py` — probe the GPU (driver, CUDA, device count) and return a structured Python dict
130
+ - `03_bash_command.py` — run an arbitrary shell command with `mimiry.run()`
mimiry-0.2.1/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # mimiry — Python SDK for Mimiry GPU compute
2
+
3
+ **Status:** alpha — early access
4
+ **Backend:** softlaunch.mimiry.com (beta)
5
+
6
+ Python-native interface for running serverless cloud GPU jobs on Mimiry, with full control over locality and providers.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install mimiry
12
+ ```
13
+
14
+ Or, for local development from a clone of this repo (editable install):
15
+
16
+ ```bash
17
+ pip install -e .
18
+ ```
19
+
20
+ ## Auth
21
+
22
+ Running jobs requires a Mimiry account. The SDK authenticates with SSH-JWT — the same SSH key you register on your account at the [Mimiry portal](https://softlaunch.mimiry.com). The fastest way to get set up is the interactive wizard, which generates a key (if needed), walks you through registering it in the portal, writes `MIMIRY_SSH_KEY` to your shell profile, and verifies the connection:
23
+
24
+ ```bash
25
+ mimiry setup # alias: mimiry init
26
+ ```
27
+
28
+ This is a one-time step — you're set going forward.
29
+
30
+ To configure auth manually instead, point the SDK at your private key:
31
+
32
+ ```bash
33
+ export MIMIRY_SSH_KEY=~/.ssh/mimiry
34
+ ```
35
+
36
+ Or pass `ssh_key_path=` explicitly to `mimiry.configure()`.
37
+
38
+ ## GPU types and providers
39
+
40
+ Mimiry sources GPUs from both local datacenters and cloud providers across Europe and the US, spanning entry-level cards up to the latest high-end accelerators. You control locality and hardware requirements, as well as which providers to use.
41
+
42
+ Always check what's currently available before selecting hardware:
43
+
44
+ ```bash
45
+ mimiry availability
46
+ ```
47
+
48
+ To filter to a single GPU family, add `--gpu-family <FAMILY>`.
49
+
50
+ ## Python version
51
+
52
+ Your local Python `major.minor` must match the Python inside your container
53
+ image. The SDK ships your function to the GPU with `cloudpickle`, which can't
54
+ move code objects across Python versions — e.g. a function pickled on 3.12
55
+ won't load on 3.10.
56
+
57
+ You don't need to think about this with the default image: it ships Python
58
+ 3.12, matching recent Ubuntu / Debian / Fedora. It only matters if you set
59
+ `image=` yourself — pick one whose `python3` matches your local interpreter.
60
+ Confirm with `python3 --version` locally and inside the image; a mismatch
61
+ shows up as a failure to deserialize your function.
62
+
63
+ ## Quickstart — one-shot function
64
+
65
+ ```python
66
+ import mimiry
67
+
68
+ @mimiry.function(
69
+ # Uses default hardware; run `mimiry availability` to choose a GPU/provider.
70
+ image="nvcr.io/nvidia/cuda:12.6.2-runtime-ubuntu24.04",
71
+ )
72
+ def gpu_info():
73
+ import subprocess
74
+ return subprocess.check_output(
75
+ ["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv"],
76
+ text=True,
77
+ )
78
+
79
+ print(gpu_info.remote())
80
+ ```
81
+
82
+ ## Quickstart — raw bash command
83
+
84
+ ```python
85
+ import mimiry
86
+
87
+ result = mimiry.run(
88
+ image="nvcr.io/nvidia/cuda:12.6.2-runtime-ubuntu24.04",
89
+ command="nvidia-smi",
90
+ )
91
+ print(result.logs)
92
+ ```
93
+
94
+ ## What works in this version
95
+
96
+ - `@mimiry.function(gpu=..., image=...)` decorator
97
+ - `.remote(*args, **kwargs)` — sync call, returns the function's return value
98
+ - `.map(iterable)` — runs the function over an iterable, sequentially
99
+ - `Image.from_registry(uri).pip_install(...).apt_install(...)` — basic image customisation (installs at container start; no real Dockerfile build)
100
+ - `mimiry.run(image, gpu, command)` — raw bash entrypoint
101
+ - SSH-JWT auth via existing key
102
+
103
+ ## Examples
104
+
105
+ See `examples/`:
106
+
107
+ - `01_hello.py` — minimal `nvidia-smi` on a GPU
108
+ - `02_cuda_probe.py` — probe the GPU (driver, CUDA, device count) and return a structured Python dict
109
+ - `03_bash_command.py` — run an arbitrary shell command with `mimiry.run()`
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "Mimiry"
7
+ version = "0.2.1"
8
+ description = "Python SDK for serverless compute"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Proprietary" }
12
+ authors = [{ name = "Oliver", email = "oliversorensen39@gmail.com" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ ]
19
+
20
+ dependencies = [
21
+ "httpx>=0.27",
22
+ "cloudpickle>=3.0",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=8",
28
+ "ruff>=0.5",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://mimiry.com"
33
+ Repository = "https://github.com/OTSorensen/mimiry-python-sdk"
34
+ Issues = "https://github.com/OTSorensen/mimiry-python-sdk/issues"
35
+
36
+ [project.scripts]
37
+ mimiry = "mimiry._cli:main"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.ruff]
43
+ line-length = 100
44
+ target-version = "py310"
mimiry-0.2.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: Mimiry
3
+ Version: 0.2.1
4
+ Summary: Python SDK for serverless compute
5
+ Author-email: Oliver <oliversorensen39@gmail.com>
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://mimiry.com
8
+ Project-URL: Repository, https://github.com/OTSorensen/mimiry-python-sdk
9
+ Project-URL: Issues, https://github.com/OTSorensen/mimiry-python-sdk/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: cloudpickle>=3.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8; extra == "dev"
20
+ Requires-Dist: ruff>=0.5; extra == "dev"
21
+
22
+ # mimiry — Python SDK for Mimiry GPU compute
23
+
24
+ **Status:** alpha — early access
25
+ **Backend:** softlaunch.mimiry.com (beta)
26
+
27
+ Python-native interface for running serverless cloud GPU jobs on Mimiry, with full control over locality and providers.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install mimiry
33
+ ```
34
+
35
+ Or, for local development from a clone of this repo (editable install):
36
+
37
+ ```bash
38
+ pip install -e .
39
+ ```
40
+
41
+ ## Auth
42
+
43
+ Running jobs requires a Mimiry account. The SDK authenticates with SSH-JWT — the same SSH key you register on your account at the [Mimiry portal](https://softlaunch.mimiry.com). The fastest way to get set up is the interactive wizard, which generates a key (if needed), walks you through registering it in the portal, writes `MIMIRY_SSH_KEY` to your shell profile, and verifies the connection:
44
+
45
+ ```bash
46
+ mimiry setup # alias: mimiry init
47
+ ```
48
+
49
+ This is a one-time step — you're set going forward.
50
+
51
+ To configure auth manually instead, point the SDK at your private key:
52
+
53
+ ```bash
54
+ export MIMIRY_SSH_KEY=~/.ssh/mimiry
55
+ ```
56
+
57
+ Or pass `ssh_key_path=` explicitly to `mimiry.configure()`.
58
+
59
+ ## GPU types and providers
60
+
61
+ Mimiry sources GPUs from both local datacenters and cloud providers across Europe and the US, spanning entry-level cards up to the latest high-end accelerators. You control locality and hardware requirements, as well as which providers to use.
62
+
63
+ Always check what's currently available before selecting hardware:
64
+
65
+ ```bash
66
+ mimiry availability
67
+ ```
68
+
69
+ To filter to a single GPU family, add `--gpu-family <FAMILY>`.
70
+
71
+ ## Python version
72
+
73
+ Your local Python `major.minor` must match the Python inside your container
74
+ image. The SDK ships your function to the GPU with `cloudpickle`, which can't
75
+ move code objects across Python versions — e.g. a function pickled on 3.12
76
+ won't load on 3.10.
77
+
78
+ You don't need to think about this with the default image: it ships Python
79
+ 3.12, matching recent Ubuntu / Debian / Fedora. It only matters if you set
80
+ `image=` yourself — pick one whose `python3` matches your local interpreter.
81
+ Confirm with `python3 --version` locally and inside the image; a mismatch
82
+ shows up as a failure to deserialize your function.
83
+
84
+ ## Quickstart — one-shot function
85
+
86
+ ```python
87
+ import mimiry
88
+
89
+ @mimiry.function(
90
+ # Uses default hardware; run `mimiry availability` to choose a GPU/provider.
91
+ image="nvcr.io/nvidia/cuda:12.6.2-runtime-ubuntu24.04",
92
+ )
93
+ def gpu_info():
94
+ import subprocess
95
+ return subprocess.check_output(
96
+ ["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv"],
97
+ text=True,
98
+ )
99
+
100
+ print(gpu_info.remote())
101
+ ```
102
+
103
+ ## Quickstart — raw bash command
104
+
105
+ ```python
106
+ import mimiry
107
+
108
+ result = mimiry.run(
109
+ image="nvcr.io/nvidia/cuda:12.6.2-runtime-ubuntu24.04",
110
+ command="nvidia-smi",
111
+ )
112
+ print(result.logs)
113
+ ```
114
+
115
+ ## What works in this version
116
+
117
+ - `@mimiry.function(gpu=..., image=...)` decorator
118
+ - `.remote(*args, **kwargs)` — sync call, returns the function's return value
119
+ - `.map(iterable)` — runs the function over an iterable, sequentially
120
+ - `Image.from_registry(uri).pip_install(...).apt_install(...)` — basic image customisation (installs at container start; no real Dockerfile build)
121
+ - `mimiry.run(image, gpu, command)` — raw bash entrypoint
122
+ - SSH-JWT auth via existing key
123
+
124
+ ## Examples
125
+
126
+ See `examples/`:
127
+
128
+ - `01_hello.py` — minimal `nvidia-smi` on a GPU
129
+ - `02_cuda_probe.py` — probe the GPU (driver, CUDA, device count) and return a structured Python dict
130
+ - `03_bash_command.py` — run an arbitrary shell command with `mimiry.run()`
@@ -0,0 +1,22 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/Mimiry.egg-info/PKG-INFO
4
+ src/Mimiry.egg-info/SOURCES.txt
5
+ src/Mimiry.egg-info/dependency_links.txt
6
+ src/Mimiry.egg-info/entry_points.txt
7
+ src/Mimiry.egg-info/requires.txt
8
+ src/Mimiry.egg-info/top_level.txt
9
+ src/mimiry/__init__.py
10
+ src/mimiry/__main__.py
11
+ src/mimiry/_auth.py
12
+ src/mimiry/_cli.py
13
+ src/mimiry/_client.py
14
+ src/mimiry/_config.py
15
+ src/mimiry/_serialization.py
16
+ src/mimiry/_session.py
17
+ src/mimiry/_setup.py
18
+ src/mimiry/_ssh.py
19
+ src/mimiry/exceptions.py
20
+ src/mimiry/function.py
21
+ src/mimiry/image.py
22
+ src/mimiry/run.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mimiry = mimiry._cli:main
@@ -0,0 +1,6 @@
1
+ httpx>=0.27
2
+ cloudpickle>=3.0
3
+
4
+ [dev]
5
+ pytest>=8
6
+ ruff>=0.5
@@ -0,0 +1 @@
1
+ mimiry
@@ -0,0 +1,35 @@
1
+ """mimiry — Python SDK for Mimiry GPU compute (softlaunch).
2
+
3
+ v0.2.0 — wraps the existing /api/compute/v1/sessions API. See README.md.
4
+ """
5
+
6
+ from mimiry._config import configure, get_config
7
+ from mimiry.exceptions import (
8
+ MimiryError,
9
+ AuthError,
10
+ SessionError,
11
+ SessionFailed,
12
+ SessionTimeout,
13
+ ResultParseError,
14
+ )
15
+ from mimiry.function import function, Function
16
+ from mimiry.image import Image
17
+ from mimiry.run import run
18
+
19
+ __version__ = "0.2.0"
20
+
21
+ __all__ = [
22
+ "__version__",
23
+ "configure",
24
+ "get_config",
25
+ "function",
26
+ "Function",
27
+ "Image",
28
+ "run",
29
+ "MimiryError",
30
+ "AuthError",
31
+ "SessionError",
32
+ "SessionFailed",
33
+ "SessionTimeout",
34
+ "ResultParseError",
35
+ ]
@@ -0,0 +1,8 @@
1
+ """Enables ``python -m mimiry ...`` (notably ``python -m mimiry setup``)."""
2
+
3
+ import sys
4
+
5
+ from mimiry._cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,188 @@
1
+ """SSH-JWT authentication against softlaunch.mimiry.com.
2
+
3
+ Ports the algorithm from .claude/skills/mimiry-softlaunch/scripts/mimiry-auth.sh.
4
+ We shell out to ``ssh-keygen`` for signing — implementing the SSH signature
5
+ format in pure Python would mean re-deriving an OpenSSH-compatible format from
6
+ the ``cryptography`` library, and ``ssh-keygen`` is universally available on
7
+ the systems Mimiry users actually run on.
8
+
9
+ Tokens last 1 hour (Mimiry default). The Token class refreshes itself when
10
+ within 5 minutes of expiry.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import base64
16
+ import os
17
+ import secrets
18
+ import shutil
19
+ import subprocess
20
+ import tempfile
21
+ import time
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+
25
+ import httpx
26
+
27
+ from mimiry.exceptions import AuthError
28
+
29
+ _TOKEN_REFRESH_BUFFER_SECONDS = 300 # refresh if < 5 min left
30
+ _DEFAULT_TOKEN_TTL_SECONDS = 3600
31
+
32
+
33
+ @dataclass
34
+ class Token:
35
+ """A short-lived JWT issued by Mimiry. Self-refreshes when near expiry."""
36
+
37
+ access_token: str
38
+ expires_at: float # unix seconds
39
+ fingerprint: str
40
+ ssh_key_path: Path
41
+ api_base: str
42
+
43
+ @property
44
+ def is_expired(self) -> bool:
45
+ return time.time() >= self.expires_at
46
+
47
+ @property
48
+ def is_near_expiry(self) -> bool:
49
+ return time.time() + _TOKEN_REFRESH_BUFFER_SECONDS >= self.expires_at
50
+
51
+ def refresh(self) -> "Token":
52
+ """Re-exchange the SSH signature for a fresh JWT. Mutates self."""
53
+ new = exchange_ssh_for_token(self.ssh_key_path, self.api_base)
54
+ self.access_token = new.access_token
55
+ self.expires_at = new.expires_at
56
+ return self
57
+
58
+ def get(self) -> str:
59
+ """Return the bearer string, refreshing if near expiry."""
60
+ if self.is_near_expiry:
61
+ self.refresh()
62
+ return self.access_token
63
+
64
+
65
+ def _normalize_key_path(key_path: str | Path) -> Path:
66
+ """Accept either the private key path or the .pub path; return the private path."""
67
+ p = Path(key_path).expanduser()
68
+ if p.suffix == ".pub":
69
+ p = p.with_suffix("")
70
+ if not p.is_file():
71
+ raise AuthError(f"SSH private key not found: {p}")
72
+ if not p.with_suffix(p.suffix + ".pub").is_file() and not Path(f"{p}.pub").is_file():
73
+ raise AuthError(f"SSH public key not found: {p}.pub")
74
+ return p
75
+
76
+
77
+ def _fingerprint(public_key_path: Path) -> str:
78
+ """Run ``ssh-keygen -lf`` to get the SHA256 fingerprint of a public key."""
79
+ if shutil.which("ssh-keygen") is None:
80
+ raise AuthError("ssh-keygen not found on PATH — required for Mimiry auth")
81
+ try:
82
+ out = subprocess.run(
83
+ ["ssh-keygen", "-lf", str(public_key_path)],
84
+ capture_output=True,
85
+ text=True,
86
+ check=True,
87
+ )
88
+ except subprocess.CalledProcessError as e:
89
+ raise AuthError(f"ssh-keygen -lf failed: {e.stderr.strip()}") from e
90
+ # Format: "<bits> <fingerprint> <comment> (<type>)"
91
+ parts = out.stdout.strip().split()
92
+ if len(parts) < 2:
93
+ raise AuthError(f"unexpected ssh-keygen output: {out.stdout!r}")
94
+ return parts[1]
95
+
96
+
97
+ def _sign(message: bytes, private_key_path: Path) -> bytes:
98
+ """Sign ``message`` with the SSH key under the ``mimiry-auth`` namespace.
99
+
100
+ Returns the raw .sig file contents (OpenSSH SSHSIG format), as Mimiry expects.
101
+ """
102
+ with tempfile.TemporaryDirectory() as tmpdir:
103
+ msg_path = Path(tmpdir) / "msg"
104
+ msg_path.write_bytes(message)
105
+ try:
106
+ subprocess.run(
107
+ ["ssh-keygen", "-Y", "sign", "-f", str(private_key_path), "-n", "mimiry-auth",
108
+ str(msg_path)],
109
+ capture_output=True,
110
+ check=True,
111
+ )
112
+ except subprocess.CalledProcessError as e:
113
+ raise AuthError(
114
+ f"ssh-keygen -Y sign failed: {e.stderr.decode(errors='replace').strip()}"
115
+ ) from e
116
+ sig_path = Path(f"{msg_path}.sig")
117
+ if not sig_path.is_file():
118
+ raise AuthError("ssh-keygen did not produce a .sig file")
119
+ return sig_path.read_bytes()
120
+
121
+
122
+ def exchange_ssh_for_token(
123
+ ssh_key_path: str | Path,
124
+ api_base: str = "https://softlaunch.mimiry.com",
125
+ ) -> Token:
126
+ """Do the SSH-signature → JWT exchange. Returns a Token."""
127
+ api_base = api_base.rstrip("/")
128
+ priv = _normalize_key_path(ssh_key_path)
129
+ pub = Path(f"{priv}.pub")
130
+
131
+ fingerprint = _fingerprint(pub)
132
+ timestamp = str(int(time.time()))
133
+ nonce = secrets.token_hex(16)
134
+
135
+ message = f"{fingerprint}\n{timestamp}\n{nonce}".encode()
136
+ signature_bytes = _sign(message, priv)
137
+ signature_b64 = base64.b64encode(signature_bytes).decode()
138
+
139
+ try:
140
+ resp = httpx.post(
141
+ f"{api_base}/api/v1/auth/token",
142
+ headers={
143
+ "X-SSH-Fingerprint": fingerprint,
144
+ "X-SSH-Signature": signature_b64,
145
+ "X-SSH-Timestamp": timestamp,
146
+ "X-SSH-Nonce": nonce,
147
+ "Content-Type": "application/json",
148
+ },
149
+ json={"expires_in": _DEFAULT_TOKEN_TTL_SECONDS},
150
+ timeout=30.0,
151
+ )
152
+ except httpx.HTTPError as e:
153
+ raise AuthError(f"token exchange request failed: {e}") from e
154
+
155
+ if resp.status_code != 200:
156
+ raise AuthError(
157
+ f"token exchange returned {resp.status_code}: {resp.text[:500]}"
158
+ )
159
+
160
+ body = resp.json()
161
+ access_token = body.get("access_token")
162
+ if not access_token:
163
+ raise AuthError(f"token exchange response missing access_token: {body}")
164
+
165
+ expires_in = body.get("expires_in", _DEFAULT_TOKEN_TTL_SECONDS)
166
+ return Token(
167
+ access_token=access_token,
168
+ expires_at=time.time() + float(expires_in),
169
+ fingerprint=fingerprint,
170
+ ssh_key_path=priv,
171
+ api_base=api_base,
172
+ )
173
+
174
+
175
+ def get_token(
176
+ ssh_key_path: str | Path | None = None,
177
+ api_base: str = "https://softlaunch.mimiry.com",
178
+ ) -> Token:
179
+ """Get a fresh Token. Falls back to ``MIMIRY_SSH_KEY`` env var when ``ssh_key_path`` is None."""
180
+ if ssh_key_path is None:
181
+ env_key = os.environ.get("MIMIRY_SSH_KEY")
182
+ if not env_key:
183
+ raise AuthError(
184
+ "no SSH key provided — pass ssh_key_path=, set MIMIRY_SSH_KEY env var, "
185
+ "or call mimiry.configure(ssh_key_path=...) first"
186
+ )
187
+ ssh_key_path = env_key
188
+ return exchange_ssh_for_token(ssh_key_path, api_base)