mainsequence 9.0.12.dev51__py3-none-any.whl → 9.0.12.dev52__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.
@@ -75,6 +75,8 @@ KEYCHAIN_ACCOUNT = "default"
75
75
  AUTH_RECORD_VERSION = 1
76
76
  # Credential entries a project `.env` must not hold. The CLI removes them and
77
77
  # never writes them; the session lives in the operating system credential store.
78
+ # The runtime credential secret and `MAINSEQUENCE_TOKEN` are no longer read: they
79
+ # stay listed so that an entry an earlier version left in a checkout is removed.
78
80
  PROJECT_ENV_CREDENTIAL_KEYS = (
79
81
  "MAINSEQUENCE_ACCESS_TOKEN",
80
82
  "MAINSEQUENCE_REFRESH_TOKEN",
@@ -805,7 +807,7 @@ def session_report() -> dict:
805
807
  refresh_expires_at = token_expiry(refresh)
806
808
 
807
809
  if auth_mode == "runtime_credential":
808
- # The credential is an ID with a bootstrap secret or an identity token file.
810
+ # The credential is an ID with the identity token file that proves it.
809
811
  authenticated = bool(access or runtime_credential_configured())
810
812
  session_expires_at = None
811
813
  elif refresh:
@@ -23,8 +23,6 @@ from mainsequence.defaults import resolve_backend_endpoint
23
23
  from mainsequence.logconf import logger
24
24
  from mainsequence.runtime_credential_exchange import (
25
25
  RUNTIME_CREDENTIAL_ID_ENV,
26
- RUNTIME_CREDENTIAL_SECRET_ENV,
27
- RUNTIME_IDENTITY_TOKEN_FILE_ENV,
28
26
  RuntimeIdentityTokenError,
29
27
  exchange_runtime_credential,
30
28
  identity_token_file_from_environment,
@@ -321,17 +319,16 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
321
319
  """
322
320
  Exchange the runtime credential the platform injected for short-lived access tokens.
323
321
 
324
- The proof is the projected workload identity token when
325
- `MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE` is set: the file is read for every
326
- exchange, and `MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET` is never read or sent.
327
- Otherwise the proof is that bootstrap secret. The token is never kept on the
328
- provider, and the secret is left out of `repr()` and `str()`. Throttled
329
- (`429`) and temporarily unavailable (`503`) exchanges are retried within the
330
- timeout budget; a rejected one (`401`) fails at once.
322
+ The proof is the projected workload identity token in the file named by
323
+ `MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE`, read for every exchange. It is the
324
+ only proof: an unset variable, or a missing, unreadable or empty file, raises
325
+ `AuthError` instead of sending the exchange. The token is never kept on the
326
+ provider.
327
+ Throttled (`429`) and temporarily unavailable (`503`) exchanges are retried
328
+ within the timeout budget; a rejected one (`401`) fails at once.
331
329
  """
332
330
 
333
331
  credential_id: str | None = None
334
- credential_secret: str | None = field(default=None, repr=False)
335
332
  token_url: str | None = None
336
333
  token_type: str = "Bearer"
337
334
  refresh_skew_seconds: int = 30
@@ -347,8 +344,6 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
347
344
  self.identity_token_file = identity_token_file_from_environment()
348
345
  else:
349
346
  self.identity_token_file = str(self.identity_token_file).strip() or None
350
- if self.credential_secret is None and not self.identity_token_file:
351
- self.credential_secret = os.getenv(RUNTIME_CREDENTIAL_SECRET_ENV)
352
347
  if not self.token_url:
353
348
  self.token_url = f"{API_ENDPOINT}/runtime-credentials/token/"
354
349
 
@@ -376,19 +371,11 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
376
371
 
377
372
  def _exchange(self) -> requests.Response:
378
373
  credential_id = (self.credential_id or "").strip()
379
- identity_token_file = self.identity_token_file
380
- credential_secret = None if identity_token_file else (self.credential_secret or "").strip()
381
374
  if not credential_id:
382
375
  raise AuthError(
383
376
  "MAINSEQUENCE_RUNTIME_CREDENTIAL_ID is required when "
384
377
  "MAINSEQUENCE_AUTH_MODE=runtime_credential."
385
378
  )
386
- if not identity_token_file and not credential_secret:
387
- raise AuthError(
388
- "MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET is required when "
389
- "MAINSEQUENCE_AUTH_MODE=runtime_credential and "
390
- f"{RUNTIME_IDENTITY_TOKEN_FILE_ENV} is not set."
391
- )
392
379
  try:
393
380
  # One budget for every attempt and wait, inside the caller's budget if any.
394
381
  with request_budget(self.timeout) as budget:
@@ -405,8 +392,7 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
405
392
  return exchange_runtime_credential(
406
393
  post,
407
394
  credential_id=credential_id,
408
- credential_secret=credential_secret,
409
- identity_token_file=identity_token_file,
395
+ identity_token_file=self.identity_token_file,
410
396
  deadline=budget.deadline,
411
397
  on_retry=self._log_exchange_retry,
412
398
  )
mainsequence/logconf.py CHANGED
@@ -25,7 +25,6 @@ from .instrumentation import OTelJSONRenderer
25
25
  from .repository_identity_security import UNSUPPORTED_SOURCE_IDENTITY_ENV_NAMES
26
26
  from .runtime_credential_exchange import (
27
27
  RUNTIME_CREDENTIAL_ID_ENV,
28
- RUNTIME_CREDENTIAL_SECRET_ENV,
29
28
  exchange_runtime_credential,
30
29
  identity_token_file_from_environment,
31
30
  runtime_credential_configured,
@@ -104,7 +103,6 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
104
103
  def _exchange_runtime_credential() -> bool:
105
104
  if not runtime_credential_configured():
106
105
  return False
107
- identity_token_file = identity_token_file_from_environment()
108
106
  token_url = f"{_backend_base_url()}/api/v1/runtime-credentials/token/"
109
107
 
110
108
  def _post(body: dict[str, str]) -> requests.Response:
@@ -120,12 +118,7 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
120
118
  token_resp = exchange_runtime_credential(
121
119
  _post,
122
120
  credential_id=(os.getenv(RUNTIME_CREDENTIAL_ID_ENV) or "").strip(),
123
- credential_secret=(
124
- None
125
- if identity_token_file
126
- else (os.getenv(RUNTIME_CREDENTIAL_SECRET_ENV) or "").strip()
127
- ),
128
- identity_token_file=identity_token_file,
121
+ identity_token_file=identity_token_file_from_environment(),
129
122
  deadline=time.monotonic() + timeout_s,
130
123
  )
131
124
  except Exception:
@@ -3,13 +3,11 @@ The runtime credential exchange, shared by the auth provider and the logger.
3
3
 
4
4
  A runtime started with ``MAINSEQUENCE_AUTH_MODE=runtime_credential`` exchanges its
5
5
  credential at ``POST /api/v1/runtime-credentials/token/`` for a short-lived access
6
- token. The request carries ``credential_id`` and exactly one proof:
7
-
8
- * ``workload_identity_token`` when ``MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE`` is
9
- set: the projected token read from that file for every exchange, because the
10
- file is rotated. The bootstrap secret is then never read or sent, and a missing,
11
- unreadable or empty file is an error, never a fallback to the secret.
12
- * ``credential_secret`` otherwise: ``MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET``.
6
+ token. The request carries exactly ``credential_id`` and ``workload_identity_token``:
7
+ the projected token read from the file named by
8
+ ``MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE`` for every exchange, because the file is
9
+ rotated. That token is the only proof. An unset variable and a missing, unreadable
10
+ or empty file are errors, raised instead of sending the request.
13
11
 
14
12
  The token exists only inside one exchange request. It is not kept on any object,
15
13
  written to the environment or a file, logged, or included in an error message.
@@ -29,7 +27,6 @@ from urllib3.exceptions import InvalidHeader
29
27
  from urllib3.util import Retry
30
28
 
31
29
  RUNTIME_CREDENTIAL_ID_ENV = "MAINSEQUENCE_RUNTIME_CREDENTIAL_ID"
32
- RUNTIME_CREDENTIAL_SECRET_ENV = "MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET"
33
30
  RUNTIME_IDENTITY_TOKEN_FILE_ENV = "MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE"
34
31
 
35
32
  # 429: the exchange is throttled. 503: verification is temporarily unavailable.
@@ -42,7 +39,7 @@ _RETRY_AFTER = Retry(total=0)
42
39
 
43
40
 
44
41
  class RuntimeIdentityTokenError(Exception):
45
- """The runtime identity token file cannot be used. The message never holds its content."""
42
+ """The identity token file is unset or cannot be used. The message never holds its content."""
46
43
 
47
44
 
48
45
  def _environment_value(name: str) -> str:
@@ -58,18 +55,22 @@ def runtime_credential_configured() -> bool:
58
55
  """
59
56
  Say whether the environment holds a runtime credential to exchange.
60
57
 
61
- That is a credential ID and one proof: the identity token file, or else the
62
- bootstrap secret. Only the presence of the setting is checked. The token file
63
- is read by the exchange itself, which reports a missing or empty file.
58
+ That is a credential ID and the identity token file that proves it. Only the
59
+ presence of both settings is checked. The token file is read by the exchange
60
+ itself, which reports a missing or empty file.
64
61
  """
65
- if not _environment_value(RUNTIME_CREDENTIAL_ID_ENV):
66
- return False
67
- if identity_token_file_from_environment():
68
- return True
69
- return bool(_environment_value(RUNTIME_CREDENTIAL_SECRET_ENV))
62
+ return bool(
63
+ _environment_value(RUNTIME_CREDENTIAL_ID_ENV) and identity_token_file_from_environment()
64
+ )
70
65
 
71
66
 
72
- def _read_identity_token(path: str) -> str:
67
+ def _read_identity_token(path: str | None) -> str:
68
+ if not path:
69
+ raise RuntimeIdentityTokenError(
70
+ f"{RUNTIME_IDENTITY_TOKEN_FILE_ENV} is required when "
71
+ "MAINSEQUENCE_AUTH_MODE=runtime_credential: a runtime credential is exchanged "
72
+ "only with the projected workload identity token in that file."
73
+ )
73
74
  source = f"The runtime identity token file {path} ({RUNTIME_IDENTITY_TOKEN_FILE_ENV})"
74
75
  try:
75
76
  with open(path, "rb") as token_file:
@@ -92,17 +93,11 @@ def _read_identity_token(path: str) -> str:
92
93
  return token
93
94
 
94
95
 
95
- def _request_body(
96
- credential_id: str,
97
- credential_secret: str | None,
98
- identity_token_file: str | None,
99
- ) -> dict[str, str]:
100
- if identity_token_file:
101
- return {
102
- "credential_id": credential_id,
103
- "workload_identity_token": _read_identity_token(identity_token_file),
104
- }
105
- return {"credential_id": credential_id, "credential_secret": credential_secret or ""}
96
+ def _request_body(credential_id: str, identity_token_file: str | None) -> dict[str, str]:
97
+ return {
98
+ "credential_id": credential_id,
99
+ "workload_identity_token": _read_identity_token(identity_token_file),
100
+ }
106
101
 
107
102
 
108
103
  def _retry_delay(response: Any, attempt: int) -> float:
@@ -120,7 +115,6 @@ def exchange_runtime_credential(
120
115
  send: Callable[[dict[str, str]], Any],
121
116
  *,
122
117
  credential_id: str,
123
- credential_secret: str | None,
124
118
  identity_token_file: str | None,
125
119
  deadline: float,
126
120
  on_retry: Callable[[int, float], None] | None = None,
@@ -135,8 +129,6 @@ def exchange_runtime_credential(
135
129
  response. It is called once per attempt.
136
130
  credential_id:
137
131
  The runtime credential ID.
138
- credential_secret:
139
- The bootstrap secret. Ignored when ``identity_token_file`` is set.
140
132
  identity_token_file:
141
133
  The projected token file. It is read again for every attempt.
142
134
  deadline:
@@ -147,12 +139,12 @@ def exchange_runtime_credential(
147
139
  A ``429`` or ``503`` answer is retried up to ``EXCHANGE_RETRIES`` times. The
148
140
  wait is the exponential backoff or the answer's ``Retry-After``, whichever is
149
141
  longer; a wait that would not end before ``deadline`` ends the retries. Any
150
- other answer, ``401`` included, is returned at once and the proof is never
151
- switched. ``RuntimeIdentityTokenError`` is raised when the token file cannot
152
- be used.
142
+ other answer, ``401`` included, is returned at once. ``RuntimeIdentityTokenError``
143
+ is raised instead of sending an attempt when ``identity_token_file`` is not set
144
+ or the file cannot be used.
153
145
  """
154
146
  for attempt in range(EXCHANGE_RETRIES + 1):
155
- response = send(_request_body(credential_id, credential_secret, identity_token_file))
147
+ response = send(_request_body(credential_id, identity_token_file))
156
148
  status = response.status_code
157
149
  if status not in RETRYABLE_EXCHANGE_STATUSES or attempt == EXCHANGE_RETRIES:
158
150
  return response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.12.dev51
3
+ Version: 9.0.12.dev52
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -15,9 +15,9 @@ mainsequence/bootstrap.py,sha256=AvG55Y46RcshVqJZsyqvUDN62CJg4CDbHu0Bvsbwfzs,291
15
15
  mainsequence/code_repository_context.py,sha256=JRrWjwpnUy8I-OjSd9hHlrcq8-Ow3Zeh_N6cYvmtA3o,28545
16
16
  mainsequence/code_repository_skills.py,sha256=AyC1HFAxQXHXNBGbarVUJS2cHd4X5HwDx6u8XBophbE,23611
17
17
  mainsequence/defaults.py,sha256=5r6s7AI-X40rpm1D3IgcNsWam7iJg4_lIxB3F_1u2z0,408
18
- mainsequence/logconf.py,sha256=RonnNIbUAP5IPdbYUHiuqBeWvRtHb5pj2YtWzHluEE4,19295
18
+ mainsequence/logconf.py,sha256=IxBuadkLTIBijNiEyid7rCkrZZP01WAQNaVhrj0OCuk,19005
19
19
  mainsequence/repository_identity_security.py,sha256=Cdywndhiy1zJFUPh-FfM90RjZ3Hg3tjhxBr64FRhmXs,1035
20
- mainsequence/runtime_credential_exchange.py,sha256=US368h7_KKorqW9yeRtIhk_Du9xjs2XNgml21dEGcLk,6428
20
+ mainsequence/runtime_credential_exchange.py,sha256=D-gDGHljKnGbMfOiUi_uJ9b5yVFkYB1CB2hikYxTdO4,6152
21
21
  mainsequence/runtime_flags.py,sha256=1eTM7VRRhnuZ_GXcmFhqAmdEKwyQYMyZcPsio88vJRQ,378
22
22
  mainsequence/scaffold_skills.py,sha256=k0V6eKC5AwbHr9wB9UGMJNF-8eJ0_1izF4-BU01KHrg,10606
23
23
  mainsequence/cli/__init__.py,sha256=sMWTnomCaISjnureLuXFRqOzkH2Y77uYEivsd-M6_8Y,28
@@ -25,7 +25,7 @@ mainsequence/cli/api.py,sha256=emn5UsIpQ0qvGLnW7MpNPYenQZjR4NJ6SGDKmLo6gHk,12740
25
25
  mainsequence/cli/browser_auth.py,sha256=Vb5MBOnPC05HLZzVNUDODobJ_p5wEIzH_h8rIlpShlE,9984
26
26
  mainsequence/cli/cli.py,sha256=zo3ptVxluTYAIeIAluGQPwgFY-_vr0SO3BthCGuVK-0,268962
27
27
  mainsequence/cli/code_repository_status.py,sha256=SHEnU8YSWFowPG6dEdE4Yd8CS1tIcJSE1MXFLfvD3Os,5975
28
- mainsequence/cli/config.py,sha256=Q4aGhSLxSeSjX2HqGgPDGg9kNAvrLhIBzWUJ-pY4lPg,45567
28
+ mainsequence/cli/config.py,sha256=TJYnM11kg5pH2O7AGWFLpNEkC9UEu0g7k2RLjhAHCzo,45724
29
29
  mainsequence/cli/docker_utils.py,sha256=eAnp9MI042cHWwKasA5IF2I7INL3A6tV2JX-cSIe7rc,10687
30
30
  mainsequence/cli/doctor.py,sha256=_1XHd72jheRDt7dEJHd6hbBSSpCPpMwwFDUOLcdkii8,4550
31
31
  mainsequence/cli/local_ops.py,sha256=15XzdIxooWqUYgWDnKB81KCmXrCdYd3AGCVazVHD1MQ,4363
@@ -47,16 +47,16 @@ mainsequence/client/models_foundry.py,sha256=JwQdPLDSO4dZI-KC0SUdmN6izmNIKMb9IzZ
47
47
  mainsequence/client/models_helpers.py,sha256=nlbHuQAylTOHAZOq4ltJQp6QvC3NW_kw7tZFosGc5rM,47311
48
48
  mainsequence/client/models_user.py,sha256=YFEClKcKZpWLCy8Gmg8tZXIHmJbkdQHkeOBJXmp0HG0,42169
49
49
  mainsequence/client/observability.py,sha256=_WrDKCaNk_TXmKElKCXC1inork6Exxxe8FRY4m1GI9s,16008
50
- mainsequence/client/utils.py,sha256=d6lXJPAOe683gb-ph9x0GJcvK5c5Pg9gr90ihDgQGug,32484
50
+ mainsequence/client/utils.py,sha256=C0h0U-fOBZw3UxIwHcBTLf8CDClAOatf_oRgjvqFth4,31637
51
51
  mainsequence/client/value_sets.py,sha256=7TA-mpuAd3hOduNgO1WTQQTuNOGaFgue-VqCsDWKaC8,6181
52
52
  mainsequence/instrumentation/__init__.py,sha256=J_9XmfjprElvz0u2t9rTyJT0kFdyKpxdbkdQiaw28_U,316
53
53
  mainsequence/instrumentation/utils.py,sha256=b-ihjSioAepeLfMpudM2SqCtvnxMqIXileHx3klBCQU,3377
54
54
  mainsequence/server/__init__.py,sha256=BHUAhtdWPDRdxX07NYO_6wrBv5mf0GsXflecO52PmUc,73
55
55
  mainsequence/server/caller_assertions.py,sha256=Me8of2Ku8in-97LWU2oY-0oD1OW6zF-xOVVpEQ0eoe4,11010
56
56
  mainsequence/server/fastapi.py,sha256=zclo7ARHKUKSTmOc0KdxUSwfsxlx6SJuaiAe-19nzWg,11465
57
- mainsequence-9.0.12.dev51.dist-info/licenses/LICENSE,sha256=b6Ecsk8wjsEby3lbSzdljR6hHiFGHBizt9iWV2p1c_I,1074
58
- mainsequence-9.0.12.dev51.dist-info/METADATA,sha256=F7sYdFmk6Ni6QFEbA2jMXCaoD6wxb_w4WSSUsZ9dPk4,7403
59
- mainsequence-9.0.12.dev51.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
60
- mainsequence-9.0.12.dev51.dist-info/entry_points.txt,sha256=XGAOq3-mF2lWWpeRJvoaWyMarRcK167mH1g124aGdUs,88
61
- mainsequence-9.0.12.dev51.dist-info/top_level.txt,sha256=BFeblM_Q9q0cUWBvTxnfWcWQEHtf5XZi9nOS-Gdfwus,28
62
- mainsequence-9.0.12.dev51.dist-info/RECORD,,
57
+ mainsequence-9.0.12.dev52.dist-info/licenses/LICENSE,sha256=b6Ecsk8wjsEby3lbSzdljR6hHiFGHBizt9iWV2p1c_I,1074
58
+ mainsequence-9.0.12.dev52.dist-info/METADATA,sha256=Kiug7ju2MO3i41EzM2Dt3iCuqNKI3WSgISbnIv4YSkk,7403
59
+ mainsequence-9.0.12.dev52.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
60
+ mainsequence-9.0.12.dev52.dist-info/entry_points.txt,sha256=XGAOq3-mF2lWWpeRJvoaWyMarRcK167mH1g124aGdUs,88
61
+ mainsequence-9.0.12.dev52.dist-info/top_level.txt,sha256=BFeblM_Q9q0cUWBvTxnfWcWQEHtf5XZi9nOS-Gdfwus,28
62
+ mainsequence-9.0.12.dev52.dist-info/RECORD,,