mainsequence 9.0.12.dev51__tar.gz → 9.0.13__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 (104) hide show
  1. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/PKG-INFO +1 -1
  2. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/config.py +3 -1
  3. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/models_user.py +14 -2
  4. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/utils.py +8 -22
  5. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/logconf.py +1 -8
  6. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/runtime_credential_exchange.py +28 -36
  7. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/PKG-INFO +1 -1
  8. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/pyproject.toml +1 -1
  9. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_auth_precedence.py +33 -26
  10. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_canonical_api_route_contract.py +1 -4
  11. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_cli_secure_storage.py +8 -10
  12. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_code_repository_runtime_context.py +4 -13
  13. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_credential_repr.py +1 -27
  14. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_http_retry_budget.py +6 -2
  15. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_runtime_credential_exchange.py +130 -53
  16. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/LICENSE +0 -0
  17. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/README.md +0 -0
  18. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/AGENTS.md +0 -0
  19. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
  20. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
  21. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
  22. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
  23. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
  24. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
  25. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
  26. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
  27. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
  28. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/__init__.py +0 -0
  29. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/__main__.py +0 -0
  30. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/_request_identity.py +0 -0
  31. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/bootstrap.py +0 -0
  32. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/__init__.py +0 -0
  33. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/api.py +0 -0
  34. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/browser_auth.py +0 -0
  35. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/cli.py +0 -0
  36. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/code_repository_status.py +0 -0
  37. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/docker_utils.py +0 -0
  38. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/doctor.py +0 -0
  39. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/local_ops.py +0 -0
  40. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/model_filters.py +0 -0
  41. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/pydantic_cli.py +0 -0
  42. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/sdk_utils.py +0 -0
  43. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/ssh_utils.py +0 -0
  44. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/ui.py +0 -0
  45. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/__init__.py +0 -0
  46. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/_transport.py +0 -0
  47. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/agent_runtime_models.py +0 -0
  48. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/base.py +0 -0
  49. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/client.py +0 -0
  50. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/compute_validation.py +0 -0
  51. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/exceptions.py +0 -0
  52. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/github_issues.py +0 -0
  53. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/inference.py +0 -0
  54. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/models_foundry.py +0 -0
  55. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/models_helpers.py +0 -0
  56. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/observability.py +0 -0
  57. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/value_sets.py +0 -0
  58. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/code_repository_context.py +0 -0
  59. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/code_repository_skills.py +0 -0
  60. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/defaults.py +0 -0
  61. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/instrumentation/__init__.py +0 -0
  62. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/instrumentation/utils.py +0 -0
  63. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/repository_identity_security.py +0 -0
  64. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/runtime_flags.py +0 -0
  65. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/scaffold_skills.py +0 -0
  66. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/server/__init__.py +0 -0
  67. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/server/caller_assertions.py +0 -0
  68. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/server/fastapi.py +0 -0
  69. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/SOURCES.txt +0 -0
  70. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/dependency_links.txt +0 -0
  71. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/entry_points.txt +0 -0
  72. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/requires.txt +0 -0
  73. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/top_level.txt +0 -0
  74. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/setup.cfg +0 -0
  75. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_auth_bootstrap.py +0 -0
  76. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_caller_assertions.py +0 -0
  77. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_cli_auth_contract.py +0 -0
  78. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_cli_browser_auth.py +0 -0
  79. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_client.py +0 -0
  80. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_code_repository_branch_contract.py +0 -0
  81. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_code_repository_documentation_contract.py +0 -0
  82. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_code_repository_skills.py +0 -0
  83. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_dependency_extras.py +0 -0
  84. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_dev_release_version.py +0 -0
  85. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_environment_resource_context.py +0 -0
  86. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_environment_response_contracts.py +0 -0
  87. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_fastapi_request_identity.py +0 -0
  88. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_github_issues_client.py +0 -0
  89. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_independent_context.py +0 -0
  90. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_inference_client.py +0 -0
  91. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_instrumentation.py +0 -0
  92. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_job_client.py +0 -0
  93. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_local_ops.py +0 -0
  94. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_logconf.py +0 -0
  95. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_models_user_request_bound_auth.py +0 -0
  96. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_notification_uid_contract.py +0 -0
  97. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_observability_client.py +0 -0
  98. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_scaffold_skills.py +0 -0
  99. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_secret_client_model.py +0 -0
  100. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_source_ownership.py +0 -0
  101. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_ssh_utils.py +0 -0
  102. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_tolerant_response_reading.py +0 -0
  103. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_tolerant_value_sets.py +0 -0
  104. {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_user_application_facts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mainsequence
3
- Version: 9.0.12.dev51
3
+ Version: 9.0.13
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -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:
@@ -983,8 +983,10 @@ class User(_CallerDirectoryReadMixin, UserApiBaseObjectOrm, BasePydanticModel):
983
983
  ``User.filter(identity_type="workload")``. Its row carries ``uid``,
984
984
  ``identity_type`` (``"workload"``), ``is_active`` and the UID of its
985
985
  workload in ``job_uid``, ``resource_release_uid`` or ``agent_uid``, without
986
- a username, email, join date or name. Fields the row does not carry are
987
- ``None``, or empty for lists.
986
+ a username, email, join date or person's name. A listed row of a workload
987
+ the caller can view also carries ``workload_name``, the name of that Job,
988
+ release or Agent; a lookup by UID does not. Fields the row does not carry
989
+ are ``None``, or empty for lists.
988
990
 
989
991
  The reverse direction is ``workload_user_uid`` on ``Job``,
990
992
  ``ResourceRelease`` and ``Agent``: the UID of the User that workload runs
@@ -1059,6 +1061,16 @@ class User(_CallerDirectoryReadMixin, UserApiBaseObjectOrm, BasePydanticModel):
1059
1061
  description="UID of the Agent a workload identity belongs to; None otherwise.",
1060
1062
  examples=[None],
1061
1063
  )
1064
+ workload_name: str | None = Field(
1065
+ None,
1066
+ title="Workload Name",
1067
+ description=(
1068
+ "Name of the Job, release or Agent a workload identity belongs to, on a "
1069
+ "listed row of a workload the caller can view. None otherwise, including "
1070
+ "on a lookup by UID and on a person."
1071
+ ),
1072
+ examples=["nightly-prices"],
1073
+ )
1062
1074
  managed_by_caller: bool | None = Field(
1063
1075
  None,
1064
1076
  title="Managed By Caller",
@@ -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
  )
@@ -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.13
4
4
  Summary: Main Sequence SDK
5
5
  Author-email: Main Sequence GmbH <dev@main-sequence.io>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mainsequence"
7
- version = "9.0.12.dev51"
7
+ version = "9.0.13"
8
8
  description = "Main Sequence SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -41,6 +41,13 @@ def _jwt_with_exp(exp: int) -> str:
41
41
  return f"{header}.{payload}.signature"
42
42
 
43
43
 
44
+ def _use_identity_token_file(monkeypatch, directory) -> str:
45
+ token_file = directory / "token"
46
+ token_file.write_text("projected-token", encoding="utf-8")
47
+ monkeypatch.setenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", str(token_file))
48
+ return str(token_file)
49
+
50
+
44
51
  def test_build_default_auth_provider_uses_jwt(monkeypatch):
45
52
  monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
46
53
  monkeypatch.delenv("MAINSEQUENCE_REFRESH_TOKEN", raising=False)
@@ -110,12 +117,12 @@ def test_logconf_refreshes_jwt_before_startup_state_request(monkeypatch):
110
117
  assert get_calls[0]["headers"]["Authorization"] == "Bearer jwt-access"
111
118
 
112
119
 
113
- def test_logconf_runtime_credential_exchanges_before_startup_state_request(monkeypatch):
120
+ def test_logconf_runtime_credential_exchanges_before_startup_state_request(monkeypatch, tmp_path):
114
121
  monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
115
122
  monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
116
123
  monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
117
124
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
118
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
125
+ _use_identity_token_file(monkeypatch, tmp_path)
119
126
  monkeypatch.setenv("JOB_RUN_UID", "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")
120
127
  monkeypatch.setenv("MAINSEQUENCE_ENDPOINT", "https://backend.example")
121
128
 
@@ -147,7 +154,7 @@ def test_logconf_runtime_credential_exchanges_before_startup_state_request(monke
147
154
  "headers": {"Content-Type": "application/json"},
148
155
  "json": {
149
156
  "credential_id": "cred-id",
150
- "credential_secret": "cred-secret",
157
+ "workload_identity_token": "projected-token",
151
158
  },
152
159
  "timeout": 10.0,
153
160
  "allow_redirects": False,
@@ -158,12 +165,12 @@ def test_logconf_runtime_credential_exchanges_before_startup_state_request(monke
158
165
  assert get_calls[0]["headers"]["Authorization"] == "Bearer runtime-access"
159
166
 
160
167
 
161
- def test_logconf_runtime_credential_retries_after_auth_failure(monkeypatch):
168
+ def test_logconf_runtime_credential_retries_after_auth_failure(monkeypatch, tmp_path):
162
169
  monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", "stale-runtime-access")
163
170
  monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
164
171
  monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
165
172
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
166
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
173
+ _use_identity_token_file(monkeypatch, tmp_path)
167
174
  monkeypatch.setenv("JOB_RUN_UID", "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")
168
175
  monkeypatch.setenv("MAINSEQUENCE_ENDPOINT", "https://backend.example")
169
176
 
@@ -197,11 +204,11 @@ def test_logconf_runtime_credential_retries_after_auth_failure(monkeypatch):
197
204
  assert os.environ["MAINSEQUENCE_ACCESS_TOKEN"] == "fresh-runtime-access"
198
205
 
199
206
 
200
- def test_runtime_credential_provider_exchanges_and_writes_access_token(monkeypatch):
207
+ def test_runtime_credential_provider_exchanges_and_writes_access_token(monkeypatch, tmp_path):
201
208
  monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
202
209
  monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
203
210
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
204
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
211
+ _use_identity_token_file(monkeypatch, tmp_path)
205
212
 
206
213
  utils = _load_mainsequence_submodule("mainsequence.client.utils")
207
214
  runtime_context_module = importlib.import_module("mainsequence.code_repository_context")
@@ -238,7 +245,7 @@ def test_runtime_credential_provider_exchanges_and_writes_access_token(monkeypat
238
245
  assert headers["Authorization"] == "Bearer runtime-access"
239
246
  assert calls[0]["json"] == {
240
247
  "credential_id": "cred-id",
241
- "credential_secret": "cred-secret",
248
+ "workload_identity_token": "projected-token",
242
249
  }
243
250
  assert calls[0]["url"].endswith("/api/v1/runtime-credentials/token/")
244
251
  assert calls[0]["headers"]["Content-Type"] == "application/json"
@@ -254,11 +261,11 @@ def test_runtime_credential_provider_exchanges_and_writes_access_token(monkeypat
254
261
  ]
255
262
 
256
263
 
257
- def test_runtime_credential_provider_reuses_valid_exchanged_access_token(monkeypatch):
264
+ def test_runtime_credential_provider_reuses_valid_exchanged_access_token(monkeypatch, tmp_path):
258
265
  monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
259
266
  monkeypatch.delenv("MAINSEQUENCE_REFRESH_TOKEN", raising=False)
260
267
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
261
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
268
+ _use_identity_token_file(monkeypatch, tmp_path)
262
269
 
263
270
  utils = _load_mainsequence_submodule("mainsequence.client.utils")
264
271
  calls = {"count": 0}
@@ -286,11 +293,11 @@ def test_runtime_credential_provider_reuses_valid_exchanged_access_token(monkeyp
286
293
 
287
294
 
288
295
  def test_runtime_credential_provider_does_not_mutate_unsupported_identity_environment(
289
- monkeypatch,
296
+ monkeypatch, tmp_path
290
297
  ):
291
298
  monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
292
299
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
293
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
300
+ _use_identity_token_file(monkeypatch, tmp_path)
294
301
  monkeypatch.setenv(
295
302
  _UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV,
296
303
  "caller-supplied-value-must-be-ignored",
@@ -325,11 +332,11 @@ def test_runtime_credential_provider_does_not_mutate_unsupported_identity_enviro
325
332
  )
326
333
 
327
334
 
328
- def test_runtime_credential_provider_force_refresh_exchanges_again(monkeypatch):
335
+ def test_runtime_credential_provider_force_refresh_exchanges_again(monkeypatch, tmp_path):
329
336
  monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
330
337
  monkeypatch.delenv("MAINSEQUENCE_REFRESH_TOKEN", raising=False)
331
338
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
332
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
339
+ _use_identity_token_file(monkeypatch, tmp_path)
333
340
 
334
341
  utils = _load_mainsequence_submodule("mainsequence.client.utils")
335
342
  calls = {"count": 0}
@@ -355,11 +362,11 @@ def test_runtime_credential_provider_force_refresh_exchanges_again(monkeypatch):
355
362
  assert calls["count"] == 2
356
363
 
357
364
 
358
- def test_runtime_credential_provider_exchanges_near_expiry_access_token(monkeypatch):
365
+ def test_runtime_credential_provider_exchanges_near_expiry_access_token(monkeypatch, tmp_path):
359
366
  monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", _jwt_with_exp(int(time.time()) + 10))
360
367
  monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
361
368
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
362
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
369
+ _use_identity_token_file(monkeypatch, tmp_path)
363
370
 
364
371
  utils = _load_mainsequence_submodule("mainsequence.client.utils")
365
372
  calls: list[dict] = []
@@ -386,12 +393,12 @@ def test_runtime_credential_provider_exchanges_near_expiry_access_token(monkeypa
386
393
  assert len(calls) == 1
387
394
 
388
395
 
389
- def test_runtime_credential_make_request_401_forces_exchange_and_retry(monkeypatch):
396
+ def test_runtime_credential_make_request_401_forces_exchange_and_retry(monkeypatch, tmp_path):
390
397
  monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
391
398
  monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", "stale-runtime-access")
392
399
  monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
393
400
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
394
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
401
+ _use_identity_token_file(monkeypatch, tmp_path)
395
402
 
396
403
  utils = _load_mainsequence_submodule("mainsequence.client.utils")
397
404
  post_calls: list[dict] = []
@@ -441,7 +448,7 @@ def test_runtime_credential_make_request_401_forces_exchange_and_retry(monkeypat
441
448
  assert len(post_calls) == 1
442
449
  assert post_calls[0]["json"] == {
443
450
  "credential_id": "cred-id",
444
- "credential_secret": "cred-secret",
451
+ "workload_identity_token": "projected-token",
445
452
  }
446
453
  assert os.environ["MAINSEQUENCE_ACCESS_TOKEN"] == "fresh-runtime-access"
447
454
  assert os.environ["MAINSEQUENCE_REFRESH_TOKEN"] == "must-not-be-used"
@@ -451,7 +458,7 @@ def test_runtime_credential_provider_requires_credential_env(monkeypatch):
451
458
  monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
452
459
  monkeypatch.delenv("MAINSEQUENCE_REFRESH_TOKEN", raising=False)
453
460
  monkeypatch.delenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", raising=False)
454
- monkeypatch.delenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", raising=False)
461
+ monkeypatch.delenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", raising=False)
455
462
 
456
463
  utils = _load_mainsequence_submodule("mainsequence.client.utils")
457
464
 
@@ -463,12 +470,12 @@ def test_runtime_credential_provider_requires_credential_env(monkeypatch):
463
470
  assert "MAINSEQUENCE_RUNTIME_CREDENTIAL_ID is required" in str(exc)
464
471
 
465
472
 
466
- def test_build_default_auth_provider_uses_runtime_credential_mode(monkeypatch):
473
+ def test_build_default_auth_provider_uses_runtime_credential_mode(monkeypatch, tmp_path):
467
474
  monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
468
475
  monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", "jwt-access")
469
476
  monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "jwt-refresh")
470
477
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
471
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
478
+ token_file = _use_identity_token_file(monkeypatch, tmp_path)
472
479
 
473
480
  utils = _load_mainsequence_submodule("mainsequence.client.utils")
474
481
 
@@ -476,15 +483,15 @@ def test_build_default_auth_provider_uses_runtime_credential_mode(monkeypatch):
476
483
 
477
484
  assert isinstance(provider, utils.RuntimeCredentialAuthProvider)
478
485
  assert provider.credential_id == "cred-id"
479
- assert provider.credential_secret == "cred-secret"
486
+ assert provider.identity_token_file == token_file
480
487
 
481
488
 
482
- def test_auth_loaders_switch_to_runtime_credential_mode(monkeypatch):
489
+ def test_auth_loaders_switch_to_runtime_credential_mode(monkeypatch, tmp_path):
483
490
  monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
484
491
  monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", "jwt-access")
485
492
  monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "jwt-refresh")
486
493
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
487
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
494
+ token_file = _use_identity_token_file(monkeypatch, tmp_path)
488
495
 
489
496
  utils = _load_mainsequence_submodule("mainsequence.client.utils")
490
497
  loaders = utils.AuthLoaders()
@@ -494,7 +501,7 @@ def test_auth_loaders_switch_to_runtime_credential_mode(monkeypatch):
494
501
 
495
502
  assert isinstance(provider, utils.RuntimeCredentialAuthProvider)
496
503
  assert provider.credential_id == "cred-id"
497
- assert provider.credential_secret == "cred-secret"
504
+ assert provider.identity_token_file == token_file
498
505
 
499
506
 
500
507
  def test_build_default_auth_provider_defaults_to_jwt_when_mode_unset(monkeypatch):
@@ -107,10 +107,7 @@ def test_client_auth_provider_routes_match_backend_contract(monkeypatch):
107
107
  access_token="access",
108
108
  refresh_token="refresh",
109
109
  )
110
- runtime_provider = utils.RuntimeCredentialAuthProvider(
111
- credential_id="credential-id",
112
- credential_secret="credential-secret",
113
- )
110
+ runtime_provider = utils.RuntimeCredentialAuthProvider(credential_id="credential-id")
114
111
 
115
112
  assert jwt_provider.obtain_url == "https://backend.example/auth/jwt-token/token/"
116
113
  assert jwt_provider.refresh_url == "https://backend.example/auth/jwt-token/token/refresh/"
@@ -1011,30 +1011,28 @@ def test_session_report_judges_the_session_by_its_refresh_token(monkeypatch, tmp
1011
1011
  def test_session_report_for_a_runtime_credential_has_no_session_expiry(monkeypatch, tmp_path):
1012
1012
  _isolate_auth(monkeypatch, tmp_path, None)
1013
1013
  monkeypatch.setattr(config, "auth_persistence_label", lambda: "test store")
1014
+ token_file = tmp_path / "token"
1015
+ token_file.write_text("projected-token", encoding="utf-8")
1014
1016
  monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
1015
1017
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
1016
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "cred-secret")
1018
+ monkeypatch.setenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", str(token_file))
1017
1019
 
1018
1020
  report = config.session_report()
1019
1021
 
1020
1022
  assert report["authenticated"] is True
1021
1023
  assert report["session_expires_at"] is None
1022
1024
  assert report["auth_mode"] == "runtime_credential"
1023
- assert "cred-secret" not in json.dumps(report)
1025
+ assert "projected-token" not in json.dumps(report)
1024
1026
 
1025
1027
 
1026
- def test_session_report_accepts_a_runtime_identity_token_file(monkeypatch, tmp_path):
1028
+ def test_session_report_needs_the_runtime_identity_token_file(monkeypatch, tmp_path):
1027
1029
  _isolate_auth(monkeypatch, tmp_path, None)
1028
1030
  monkeypatch.setattr(config, "auth_persistence_label", lambda: "test store")
1029
- token_file = tmp_path / "token"
1030
- token_file.write_text("projected-token", encoding="utf-8")
1031
1031
  monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
1032
1032
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
1033
- monkeypatch.delenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", raising=False)
1034
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", str(token_file))
1033
+ monkeypatch.delenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", raising=False)
1035
1034
 
1036
1035
  report = config.session_report()
1037
1036
 
1038
- assert report["authenticated"] is True
1039
- assert report["session_expires_at"] is None
1040
- assert "projected-token" not in json.dumps(report)
1037
+ assert report["authenticated"] is False
1038
+ assert report["auth_mode"] == "runtime_credential"
@@ -368,21 +368,12 @@ def test_authenticated_runtime_context_detects_git_drift(monkeypatch):
368
368
  code_repository_context.validate_code_repository_source_context(context=context)
369
369
 
370
370
 
371
- @pytest.mark.parametrize(
372
- ("proof_variable", "proof_value"),
373
- [
374
- ("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "credential-secret"),
375
- ("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", "/var/run/secrets/runtime-identity/token"),
376
- ],
377
- )
378
- def test_configured_runtime_credential_exchanges_context_before_resolution(
379
- monkeypatch, proof_variable, proof_value
380
- ):
371
+ def test_configured_runtime_credential_exchanges_context_before_resolution(monkeypatch):
381
372
  monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
382
373
  monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "credential-id")
383
- monkeypatch.delenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", raising=False)
384
- monkeypatch.delenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", raising=False)
385
- monkeypatch.setenv(proof_variable, proof_value)
374
+ monkeypatch.setenv(
375
+ "MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", "/var/run/secrets/runtime-identity/token"
376
+ )
386
377
  events = []
387
378
  monkeypatch.setattr(
388
379
  code_repository_context,
@@ -15,21 +15,15 @@ from mainsequence.cli.browser_auth import BrowserAuthCallback
15
15
  from mainsequence.client.agent_runtime_models import AgentSessionRuntimeAccess
16
16
  from mainsequence.client.models_foundry import Secret
17
17
  from mainsequence.client.models_helpers import ResourceReleaseRuntimeAccess
18
- from mainsequence.client.utils import (
19
- JWTAuthProvider,
20
- RuntimeCredentialAuthProvider,
21
- SessionJWTAuthProvider,
22
- )
18
+ from mainsequence.client.utils import JWTAuthProvider, SessionJWTAuthProvider
23
19
  from tests.client.support import _ready_runtime_contract, _runtime_access_payload
24
20
 
25
- BOOTSTRAP_SECRET = "bootstrap-secret-7c1d"
26
21
  ACCESS_TOKEN = "access-token-5e2a"
27
22
  REFRESH_TOKEN = "refresh-token-9b4f"
28
23
  GATEWAY_TOKEN = "gateway-token-3a8e"
29
24
  RELEASE_TOKEN = "release-token-6d0c"
30
25
  AUTHORIZATION_CODE = "authorization-code-2f7b"
31
26
  SECRET_VALUE = "secret-value-8e5a"
32
- TOKEN_URL = "https://backend.example/api/v1/runtime-credentials/token/"
33
27
 
34
28
 
35
29
  def _shown(obj, caplog) -> str:
@@ -44,26 +38,6 @@ def _error_text(raised: pytest.ExceptionInfo) -> str:
44
38
  return f"{raised.value}\n{raised.value!r}"
45
39
 
46
40
 
47
- @pytest.mark.parametrize("source", ["environment", "argument"])
48
- def test_runtime_credential_provider_leaves_the_bootstrap_secret_out(monkeypatch, caplog, source):
49
- monkeypatch.delenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", raising=False)
50
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
51
- if source == "environment":
52
- monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", BOOTSTRAP_SECRET)
53
- provider = RuntimeCredentialAuthProvider(token_url=TOKEN_URL)
54
- else:
55
- monkeypatch.delenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", raising=False)
56
- provider = RuntimeCredentialAuthProvider(
57
- credential_secret=BOOTSTRAP_SECRET, token_url=TOKEN_URL
58
- )
59
-
60
- shown = _shown(provider, caplog)
61
-
62
- assert provider.credential_secret == BOOTSTRAP_SECRET
63
- assert BOOTSTRAP_SECRET not in shown
64
- assert "credential_id='cred-id'" in shown
65
-
66
-
67
41
  def test_jwt_provider_leaves_its_tokens_out(monkeypatch, caplog):
68
42
  monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", ACCESS_TOKEN)
69
43
  monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", REFRESH_TOKEN)