bladekit 4.5.0__py3-none-any.whl

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.
@@ -0,0 +1,148 @@
1
+ """Minimal client for the *runner-internal* API, used from inside a container.
2
+
3
+ Only the SDK-in-runner needs this — currently just ``volume.commit()``
4
+ (runner-protocol.md §11). It authenticates with the run's ephemeral token and
5
+ talks to ``{FABRIC_API_URL}/internal/runner/v1/…`` (``FORGE_API_URL`` as injected
6
+ by the control plane today — see :mod:`bladekit._env`).
7
+
8
+ Where the token comes from (PAAS-250): the runner (PID 1) reads
9
+ ``FORGE_RUNNER_TOKEN`` at boot and REMOVES it from ``os.environ`` before any user
10
+ code is imported, so the function's subprocesses and ``env`` dumps no longer
11
+ carry it. The SDK therefore asks the runner in-process —
12
+ ``forge_runner.runtime.credentials()`` — and only falls back to the environment
13
+ for a runner too old to scrub it. See :func:`runner_credentials`.
14
+
15
+ The runner never talks to the agent (invariant): commits are orchestrated by
16
+ the control plane. ``httpx`` is imported lazily to keep ``import bladekit`` cheap.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from types import ModuleType
22
+ from typing import Any, NamedTuple, Optional
23
+
24
+ from . import _env
25
+ from . import exceptions as exc
26
+
27
+
28
+ class RunnerCredentials(NamedTuple):
29
+ """The run's API base URL and ephemeral token, with where they came from."""
30
+
31
+ api_url: str
32
+ token: str
33
+ source: str # "runner" (in-process context) | "env (FABRIC_RUNNER_TOKEN)" | "env (FORGE_RUNNER_TOKEN)"
34
+
35
+
36
+ def runner_credentials() -> Optional[RunnerCredentials]:
37
+ """Return the credentials of the run this process belongs to, or ``None``.
38
+
39
+ 1. ``forge_runner.runtime.credentials()`` — the runner's in-process context,
40
+ set before user code is imported. The runner package is only importable
41
+ inside an AI Fabric container, so this is ``None`` on a laptop.
42
+ 2. ``FABRIC_API_URL`` + ``FABRIC_RUNNER_TOKEN`` (or their ``FORGE_*`` spelling,
43
+ which is what the control plane injects today) in the environment — the legacy
44
+ channel, still honoured for a runner older than 1.4 that does not scrub
45
+ the variable. A current runner leaves nothing here to read.
46
+ """
47
+ _runner_runtime: Optional[ModuleType]
48
+ try:
49
+ from forge_runner import runtime as _runner_runtime # type: ignore[import-not-found,no-redef]
50
+ except Exception: # noqa: BLE001 — not in a container, or a foreign package
51
+ _runner_runtime = None
52
+ if _runner_runtime is not None:
53
+ get = getattr(_runner_runtime, "credentials", None)
54
+ if callable(get):
55
+ got = get()
56
+ if got:
57
+ api_url, token = got
58
+ if api_url and token:
59
+ return RunnerCredentials(str(api_url), str(token), "runner")
60
+ base = _env.get("API_URL")
61
+ token, token_var = _env.lookup("RUNNER_TOKEN")
62
+ if base and token:
63
+ return RunnerCredentials(base, token, f"env ({token_var})")
64
+ return None
65
+
66
+ # 404 VOLUME_COMMIT_NOT_FOUND / VOLUME_NOT_MOUNTED etc. map onto these.
67
+ _CODE_EXC = {
68
+ "VOLUME_NOT_MOUNTED": exc.VolumeError,
69
+ "VOLUME_READ_ONLY": exc.VolumeCommitError,
70
+ "VOLUME_COMMIT_IN_PROGRESS": exc.VolumeCommitError,
71
+ "VOLUME_COMMIT_NOT_FOUND": exc.VolumeCommitError,
72
+ }
73
+
74
+
75
+ class RunnerApiClient:
76
+ """Synchronous client for ``/internal/runner/v1/*`` (run-token auth)."""
77
+
78
+ def __init__(self, base_url: str, runner_token: str) -> None:
79
+ import httpx
80
+
81
+ self.base_url = base_url.rstrip("/")
82
+ self._http = httpx.Client(
83
+ base_url=self.base_url,
84
+ headers={
85
+ "Authorization": f"Bearer {runner_token}",
86
+ "User-Agent": "forge-runner-sdk/0.1.0",
87
+ },
88
+ timeout=30.0,
89
+ )
90
+
91
+ @classmethod
92
+ def from_env(cls) -> "RunnerApiClient":
93
+ """Build the client from the run's credentials (see :func:`runner_credentials`).
94
+
95
+ The name predates PAAS-250: the primary source is now the runner's
96
+ in-process context, not the environment.
97
+ """
98
+ creds = runner_credentials()
99
+ if creds is None:
100
+ raise exc.VolumeError(
101
+ "volume.commit() must run inside an AI Fabric container "
102
+ "(no runner credentials: not started by forge-runner)."
103
+ )
104
+ return cls(creds.api_url, creds.token)
105
+
106
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
107
+ import httpx
108
+
109
+ try:
110
+ resp = self._http.request(method, path, **kwargs)
111
+ except httpx.HTTPError as e:
112
+ raise exc.ConnectionFailed(
113
+ f"runner API call to {path} failed: {e}"
114
+ ) from e
115
+ if resp.status_code >= 400:
116
+ code = "INTERNAL"
117
+ message = f"HTTP {resp.status_code}"
118
+ try:
119
+ body = resp.json()
120
+ err = body.get("error") if isinstance(body, dict) else None
121
+ if isinstance(err, dict):
122
+ code = str(err.get("code", code))
123
+ message = str(err.get("message", message))
124
+ except ValueError:
125
+ pass
126
+ exc_cls = _CODE_EXC.get(code)
127
+ if exc_cls is exc.VolumeError:
128
+ raise exc_cls(message)
129
+ if exc_cls is exc.VolumeCommitError:
130
+ raise exc_cls(code, message)
131
+ raise exc.APIError(code, message, status_code=resp.status_code)
132
+ if resp.content:
133
+ return resp.json()
134
+ return None
135
+
136
+ def commit_volume(self, volume_id: str) -> dict:
137
+ """POST the commit (202 + VolumeCommit). Idempotent on network retry."""
138
+ return self._request(
139
+ "POST", f"/internal/runner/v1/volumes/{volume_id}/commit"
140
+ )
141
+
142
+ def get_volume_commit(self, commit_id: str) -> dict:
143
+ return self._request(
144
+ "GET", f"/internal/runner/v1/volume-commits/{commit_id}"
145
+ )
146
+
147
+ def close(self) -> None:
148
+ self._http.close()