mainsequence 9.0.7.dev42__tar.gz → 9.0.8__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.
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/PKG-INFO +1 -1
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +48 -21
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/config.py +3 -7
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/models_user.py +24 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/utils.py +61 -19
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/code_repository_context.py +3 -3
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/logconf.py +29 -10
- mainsequence-9.0.8/mainsequence/runtime_credential_exchange.py +168 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/server/caller_assertions.py +26 -2
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/server/fastapi.py +5 -1
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence.egg-info/PKG-INFO +1 -1
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence.egg-info/SOURCES.txt +2 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/pyproject.toml +1 -1
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_auth_precedence.py +1 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_caller_assertions.py +41 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_cli_secure_storage.py +17 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_code_repository_runtime_context.py +13 -2
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_fastapi_request_identity.py +19 -0
- mainsequence-9.0.8/tests/test_runtime_credential_exchange.py +383 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/LICENSE +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/README.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/__init__.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/__main__.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/_request_identity.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/bootstrap.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/api.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/cli.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/code_repository_status.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/cli/ui.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/__init__.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/_transport.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/base.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/client.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/github_issues.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/inference.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/observability.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/client/value_sets.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/code_repository_skills.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/defaults.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/repository_identity_security.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence/server/__init__.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/setup.cfg +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_auth_bootstrap.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_canonical_api_route_contract.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_cli_auth_contract.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_client.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_code_repository_branch_contract.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_code_repository_documentation_contract.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_code_repository_skills.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_dependency_extras.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_dev_release_version.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_environment_resource_context.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_environment_response_contracts.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_github_issues_client.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_http_retry_budget.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_independent_context.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_inference_client.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_instrumentation.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_job_client.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_local_ops.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_logconf.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_observability_client.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_scaffold_skills.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_secret_client_model.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_source_ownership.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_ssh_utils.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_tolerant_response_reading.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_tolerant_value_sets.py +0 -0
- {mainsequence-9.0.7.dev42 → mainsequence-9.0.8}/tests/test_user_application_facts.py +0 -0
|
@@ -92,46 +92,71 @@ Record:
|
|
|
92
92
|
- whether the API uses backend transport or a contract-defined direct
|
|
93
93
|
development transport
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
Install the SDK's request-identity integration once when creating the FastAPI
|
|
96
|
+
application. Inspect the existing application factory first: the generated
|
|
97
|
+
platform template already calls `install_request_identity(app)`, and a second
|
|
98
|
+
installation raises an error. For an application without that setup:
|
|
98
99
|
|
|
99
100
|
```python
|
|
100
|
-
from fastapi import FastAPI
|
|
101
|
+
from fastapi import FastAPI
|
|
102
|
+
from mainsequence.client import User
|
|
103
|
+
from mainsequence.server.fastapi import install_request_identity
|
|
101
104
|
|
|
102
105
|
|
|
103
106
|
app = FastAPI()
|
|
107
|
+
install_request_identity(app)
|
|
104
108
|
|
|
105
109
|
|
|
106
110
|
@app.get("/me")
|
|
107
|
-
def get_me(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"username": request.state.user.username,
|
|
111
|
-
}
|
|
111
|
+
def get_me() -> dict[str, str | None]:
|
|
112
|
+
user = User.get_logged_user()
|
|
113
|
+
return {"uid": user.uid, "username": user.username}
|
|
112
114
|
```
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
Use `User.get_logged_user()` in authenticated synchronous or asynchronous
|
|
117
|
+
handlers and their shared services. The middleware verifies the caller before
|
|
118
|
+
the handler, binds one isolated request context, and invalidates it on
|
|
119
|
+
completion, errors, or cancellation. Repeated getter calls do not perform
|
|
120
|
+
additional authentication requests.
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
-
|
|
122
|
+
The same installation handles both HTTP execution modes:
|
|
123
|
+
|
|
124
|
+
- Local execution without hosted deployment markers validates the incoming
|
|
125
|
+
Bearer token against `MAINSEQUENCE_ENDPOINT/api/v1/users/me/`.
|
|
126
|
+
- Platform-hosted execution verifies the gateway's signed caller assertion
|
|
127
|
+
using deployment-owned issuer, public-key, release, and Environment settings.
|
|
128
|
+
Hosted configuration cannot select local mode, and failed proof never falls
|
|
129
|
+
back to UID headers or the process's credentials.
|
|
130
|
+
|
|
131
|
+
Route code must not select the mode, parse authentication headers, or implement
|
|
132
|
+
another caller resolver. The launcher checks the application's installation
|
|
133
|
+
declaration before serving; it does not install the middleware itself.
|
|
134
|
+
|
|
135
|
+
The identity contains canonical `uid` and optional `username`; signed HTTP
|
|
136
|
+
proofs contain no username. `request.state.user` and `request.state.user_uid`
|
|
137
|
+
remain projections of the same identity, not a separate resolver. Outside an
|
|
138
|
+
authenticated request, including public routes, `User.get_logged_user()` raises
|
|
139
|
+
`RequestIdentityError`.
|
|
121
140
|
|
|
122
141
|
On a protected route, this identity is the human making the current HTTP
|
|
123
142
|
request. It is not the release creator, deployment owner, runtime workload
|
|
124
|
-
principal, or
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
principal, or the process account returned by `User.get_authenticated_user_details()`.
|
|
144
|
+
Caller authentication is not resource authorization; apply the endpoint's
|
|
145
|
+
application policy using the verified user UID.
|
|
146
|
+
|
|
147
|
+
WebSockets retain the platform's gateway ticket/header contract rather than
|
|
148
|
+
the HTTP Bearer/assertion flow; the getter is available while the authenticated
|
|
149
|
+
connection is active. See the version-matched
|
|
150
|
+
[FastAPI identity documentation](https://mainsequence-sdk.github.io/mainsequence-sdk/knowledge/fastapi/)
|
|
151
|
+
for the lifecycle and trust contract.
|
|
128
152
|
|
|
129
153
|
### Public provider callbacks and webhooks
|
|
130
154
|
|
|
131
155
|
An external OAuth redirect or webhook cannot supply a Main Sequence Bearer token.
|
|
132
156
|
For a FastAPI release that needs one, use the platform's exact `public_ingress`
|
|
133
157
|
policy. CORS configuration and an app-declared route alone do not make a path
|
|
134
|
-
public. Ordinary routes
|
|
158
|
+
public. Ordinary routes still require an authenticated caller through the
|
|
159
|
+
request-identity integration.
|
|
135
160
|
|
|
136
161
|
1. Implement the provider handler in the FastAPI app loaded by the workflow's
|
|
137
162
|
`source_path`. Declare only its exact `GET` or `POST` method and literal
|
|
@@ -166,7 +191,9 @@ public. Ordinary routes remain Bearer authenticated.
|
|
|
166
191
|
4. Send a provider-style request without a Main Sequence Bearer token to the
|
|
167
192
|
exact public pair. Check that an unlisted path or wrong method is denied.
|
|
168
193
|
An admitted request has `request.state.user` and `user_uid` set to `None`
|
|
169
|
-
and `request.state.auth_outcome == "public_ingress"
|
|
194
|
+
and `request.state.auth_outcome == "public_ingress"`;
|
|
195
|
+
`User.get_logged_user()` raises `RequestIdentityError` in that anonymous
|
|
196
|
+
scope. The handler must verify
|
|
170
197
|
OAuth state and exchange codes, or verify the webhook signature against the
|
|
171
198
|
original body and reject replays. Public admission supplies no User or
|
|
172
199
|
provider identity. Keep provider secrets and live codes out of workflow YAML,
|
|
@@ -29,6 +29,7 @@ from keyring import core as keyring_core
|
|
|
29
29
|
from keyring import errors as keyring_errors
|
|
30
30
|
|
|
31
31
|
from mainsequence.defaults import CANONICAL_BACKEND_ENV, STANDARD_BACKEND_URL
|
|
32
|
+
from mainsequence.runtime_credential_exchange import runtime_credential_configured
|
|
32
33
|
|
|
33
34
|
APP_NAME = "MainSequenceCLI"
|
|
34
35
|
|
|
@@ -804,13 +805,8 @@ def session_report() -> dict:
|
|
|
804
805
|
refresh_expires_at = token_expiry(refresh)
|
|
805
806
|
|
|
806
807
|
if auth_mode == "runtime_credential":
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
or (
|
|
810
|
-
(os.environ.get("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID") or "").strip()
|
|
811
|
-
and (os.environ.get("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET") or "").strip()
|
|
812
|
-
)
|
|
813
|
-
)
|
|
808
|
+
# The credential is an ID with a bootstrap secret or an identity token file.
|
|
809
|
+
authenticated = bool(access or runtime_credential_configured())
|
|
814
810
|
session_expires_at = None
|
|
815
811
|
elif refresh:
|
|
816
812
|
authenticated = refresh_expires_at is None or refresh_expires_at > now
|
|
@@ -37,6 +37,22 @@ class RequestUserIdentity(BaseModel):
|
|
|
37
37
|
title="Username",
|
|
38
38
|
description="Optional display name authenticated by the runtime gateway.",
|
|
39
39
|
)
|
|
40
|
+
team_uids: tuple[str, ...] = Field(
|
|
41
|
+
(),
|
|
42
|
+
title="Team UIDs",
|
|
43
|
+
description=(
|
|
44
|
+
"Canonical UIDs of the caller's active teams, as signed by the platform "
|
|
45
|
+
"in the caller assertion. Empty when the request carries no such facts."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
is_organization_admin: bool = Field(
|
|
49
|
+
False,
|
|
50
|
+
title="Organization admin",
|
|
51
|
+
description=(
|
|
52
|
+
"Whether the platform signed the caller as an admin of the application's "
|
|
53
|
+
"Organization. False when the request carries no such facts."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
40
56
|
|
|
41
57
|
@field_validator("uid", mode="before")
|
|
42
58
|
@classmethod
|
|
@@ -46,6 +62,14 @@ class RequestUserIdentity(BaseModel):
|
|
|
46
62
|
except (AttributeError, TypeError, ValueError) as exc:
|
|
47
63
|
raise ValueError("Request user UID must be a valid UUID.") from exc
|
|
48
64
|
|
|
65
|
+
@field_validator("team_uids", mode="before")
|
|
66
|
+
@classmethod
|
|
67
|
+
def _canonicalize_team_uids(cls, value: Any) -> tuple[str, ...]:
|
|
68
|
+
try:
|
|
69
|
+
return tuple(str(UUID(str(item).strip())) for item in value or ())
|
|
70
|
+
except (AttributeError, TypeError, ValueError) as exc:
|
|
71
|
+
raise ValueError("Team UIDs must be valid UUIDs.") from exc
|
|
72
|
+
|
|
49
73
|
@field_validator("username", mode="before")
|
|
50
74
|
@classmethod
|
|
51
75
|
def _normalize_username(cls, value: Any) -> str | None:
|
|
@@ -21,6 +21,14 @@ from requests.structures import CaseInsensitiveDict
|
|
|
21
21
|
|
|
22
22
|
from mainsequence.defaults import resolve_backend_endpoint
|
|
23
23
|
from mainsequence.logconf import logger
|
|
24
|
+
from mainsequence.runtime_credential_exchange import (
|
|
25
|
+
RUNTIME_CREDENTIAL_ID_ENV,
|
|
26
|
+
RUNTIME_CREDENTIAL_SECRET_ENV,
|
|
27
|
+
RUNTIME_IDENTITY_TOKEN_FILE_ENV,
|
|
28
|
+
RuntimeIdentityTokenError,
|
|
29
|
+
exchange_runtime_credential,
|
|
30
|
+
identity_token_file_from_environment,
|
|
31
|
+
)
|
|
24
32
|
|
|
25
33
|
from ._transport import DEFAULT_ALLOWED_METHODS as DEFAULT_ALLOWED_METHODS
|
|
26
34
|
from ._transport import DEFAULT_STATUS_FORCELIST as DEFAULT_STATUS_FORCELIST
|
|
@@ -309,6 +317,17 @@ class SessionJWTAuthProvider(BaseAuthProvider):
|
|
|
309
317
|
|
|
310
318
|
@dataclass
|
|
311
319
|
class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
320
|
+
"""
|
|
321
|
+
Exchange the runtime credential the platform injected for short-lived access tokens.
|
|
322
|
+
|
|
323
|
+
The proof is the projected workload identity token when
|
|
324
|
+
`MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE` is set: the file is read for every
|
|
325
|
+
exchange, and `MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET` is never read or sent.
|
|
326
|
+
Otherwise the proof is that bootstrap secret. The token is never kept on the
|
|
327
|
+
provider. Throttled (`429`) and temporarily unavailable (`503`) exchanges are
|
|
328
|
+
retried within the timeout budget; a rejected one (`401`) fails at once.
|
|
329
|
+
"""
|
|
330
|
+
|
|
312
331
|
credential_id: str | None = None
|
|
313
332
|
credential_secret: str | None = None
|
|
314
333
|
token_url: str | None = None
|
|
@@ -316,13 +335,18 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
|
316
335
|
refresh_skew_seconds: int = 30
|
|
317
336
|
timeout: tuple[float, float] = DEFAULT_TIMEOUT
|
|
318
337
|
expires_at: float | None = None
|
|
338
|
+
identity_token_file: str | None = None
|
|
319
339
|
_lock: threading.RLock = field(default_factory=threading.RLock, init=False, repr=False)
|
|
320
340
|
|
|
321
341
|
def __post_init__(self):
|
|
322
342
|
if self.credential_id is None:
|
|
323
|
-
self.credential_id = os.getenv(
|
|
324
|
-
if self.
|
|
325
|
-
self.
|
|
343
|
+
self.credential_id = os.getenv(RUNTIME_CREDENTIAL_ID_ENV)
|
|
344
|
+
if self.identity_token_file is None:
|
|
345
|
+
self.identity_token_file = identity_token_file_from_environment()
|
|
346
|
+
else:
|
|
347
|
+
self.identity_token_file = str(self.identity_token_file).strip() or None
|
|
348
|
+
if self.credential_secret is None and not self.identity_token_file:
|
|
349
|
+
self.credential_secret = os.getenv(RUNTIME_CREDENTIAL_SECRET_ENV)
|
|
326
350
|
if not self.token_url:
|
|
327
351
|
self.token_url = f"{API_ENDPOINT}/runtime-credentials/token/"
|
|
328
352
|
|
|
@@ -344,20 +368,48 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
|
344
368
|
|
|
345
369
|
return exp <= int(time.time()) + self.refresh_skew_seconds
|
|
346
370
|
|
|
347
|
-
|
|
371
|
+
@staticmethod
|
|
372
|
+
def _log_exchange_retry(status: int, delay: float) -> None:
|
|
373
|
+
logger.warning(f"Runtime credential exchange returned {status}; retrying in {delay:.1f} s.")
|
|
374
|
+
|
|
375
|
+
def _exchange(self) -> requests.Response:
|
|
348
376
|
credential_id = (self.credential_id or "").strip()
|
|
349
|
-
|
|
377
|
+
identity_token_file = self.identity_token_file
|
|
378
|
+
credential_secret = None if identity_token_file else (self.credential_secret or "").strip()
|
|
350
379
|
if not credential_id:
|
|
351
380
|
raise AuthError(
|
|
352
381
|
"MAINSEQUENCE_RUNTIME_CREDENTIAL_ID is required when "
|
|
353
382
|
"MAINSEQUENCE_AUTH_MODE=runtime_credential."
|
|
354
383
|
)
|
|
355
|
-
if not credential_secret:
|
|
384
|
+
if not identity_token_file and not credential_secret:
|
|
356
385
|
raise AuthError(
|
|
357
386
|
"MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET is required when "
|
|
358
|
-
"MAINSEQUENCE_AUTH_MODE=runtime_credential
|
|
387
|
+
"MAINSEQUENCE_AUTH_MODE=runtime_credential and "
|
|
388
|
+
f"{RUNTIME_IDENTITY_TOKEN_FILE_ENV} is not set."
|
|
359
389
|
)
|
|
360
|
-
|
|
390
|
+
try:
|
|
391
|
+
# One budget for every attempt and wait, inside the caller's budget if any.
|
|
392
|
+
with request_budget(self.timeout) as budget:
|
|
393
|
+
|
|
394
|
+
def post(body: dict[str, str]) -> requests.Response:
|
|
395
|
+
return requests.post(
|
|
396
|
+
self.token_url,
|
|
397
|
+
json=body,
|
|
398
|
+
headers={"Content-Type": "application/json"},
|
|
399
|
+
timeout=budget.timeout(),
|
|
400
|
+
allow_redirects=False,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return exchange_runtime_credential(
|
|
404
|
+
post,
|
|
405
|
+
credential_id=credential_id,
|
|
406
|
+
credential_secret=credential_secret,
|
|
407
|
+
identity_token_file=identity_token_file,
|
|
408
|
+
deadline=budget.deadline,
|
|
409
|
+
on_retry=self._log_exchange_retry,
|
|
410
|
+
)
|
|
411
|
+
except RuntimeIdentityTokenError as exc:
|
|
412
|
+
raise AuthError(str(exc)) from None
|
|
361
413
|
|
|
362
414
|
def refresh(
|
|
363
415
|
self,
|
|
@@ -370,17 +422,7 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
|
370
422
|
if not force and not self._needs_exchange():
|
|
371
423
|
return
|
|
372
424
|
|
|
373
|
-
|
|
374
|
-
response = requests.post(
|
|
375
|
-
self.token_url,
|
|
376
|
-
json={
|
|
377
|
-
"credential_id": credential_id,
|
|
378
|
-
"credential_secret": credential_secret,
|
|
379
|
-
},
|
|
380
|
-
headers={"Content-Type": "application/json"},
|
|
381
|
-
timeout=remaining_timeout(self.timeout),
|
|
382
|
-
allow_redirects=False,
|
|
383
|
-
)
|
|
425
|
+
response = self._exchange()
|
|
384
426
|
if response.status_code < 200 or response.status_code >= 300:
|
|
385
427
|
raise AuthError(
|
|
386
428
|
f"Runtime credential exchange failed with status {response.status_code}."
|
|
@@ -10,6 +10,8 @@ from dataclasses import dataclass, replace
|
|
|
10
10
|
from typing import Any, Literal
|
|
11
11
|
from urllib.parse import urlsplit
|
|
12
12
|
|
|
13
|
+
from mainsequence.runtime_credential_exchange import runtime_credential_configured
|
|
14
|
+
|
|
13
15
|
CodeRepositoryContextStatus = Literal[
|
|
14
16
|
"resolved",
|
|
15
17
|
"code_repository_branch_not_registered",
|
|
@@ -204,9 +206,7 @@ def _exchange_authenticated_runtime_context_if_configured() -> None:
|
|
|
204
206
|
return
|
|
205
207
|
if (os.getenv("MAINSEQUENCE_AUTH_MODE") or "").strip().lower() != "runtime_credential":
|
|
206
208
|
return
|
|
207
|
-
if not (
|
|
208
|
-
return
|
|
209
|
-
if not (os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET") or "").strip():
|
|
209
|
+
if not runtime_credential_configured():
|
|
210
210
|
return
|
|
211
211
|
|
|
212
212
|
from mainsequence.client.utils import AuthError, loaders
|
|
@@ -5,6 +5,7 @@ import logging
|
|
|
5
5
|
import logging.config
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
+
import time
|
|
8
9
|
import traceback
|
|
9
10
|
from collections.abc import Mapping
|
|
10
11
|
from importlib.metadata import PackageNotFoundError
|
|
@@ -22,6 +23,13 @@ from structlog.stdlib import BoundLogger
|
|
|
22
23
|
from .defaults import resolve_backend_endpoint
|
|
23
24
|
from .instrumentation import OTelJSONRenderer
|
|
24
25
|
from .repository_identity_security import UNSUPPORTED_SOURCE_IDENTITY_ENV_NAMES
|
|
26
|
+
from .runtime_credential_exchange import (
|
|
27
|
+
RUNTIME_CREDENTIAL_ID_ENV,
|
|
28
|
+
RUNTIME_CREDENTIAL_SECRET_ENV,
|
|
29
|
+
exchange_runtime_credential,
|
|
30
|
+
identity_token_file_from_environment,
|
|
31
|
+
runtime_credential_configured,
|
|
32
|
+
)
|
|
25
33
|
from .runtime_flags import is_running_in_pod
|
|
26
34
|
|
|
27
35
|
logger = None
|
|
@@ -94,20 +102,31 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
|
|
|
94
102
|
return headers, False
|
|
95
103
|
|
|
96
104
|
def _exchange_runtime_credential() -> bool:
|
|
97
|
-
|
|
98
|
-
credential_secret = (os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET") or "").strip()
|
|
99
|
-
if not credential_id or not credential_secret:
|
|
105
|
+
if not runtime_credential_configured():
|
|
100
106
|
return False
|
|
107
|
+
identity_token_file = identity_token_file_from_environment()
|
|
108
|
+
token_url = f"{_backend_base_url()}/api/v1/runtime-credentials/token/"
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
def _post(body: dict[str, str]) -> requests.Response:
|
|
111
|
+
return requests.post(
|
|
112
|
+
token_url,
|
|
105
113
|
headers={"Content-Type": "application/json"},
|
|
106
|
-
json=
|
|
107
|
-
"credential_id": credential_id,
|
|
108
|
-
"credential_secret": credential_secret,
|
|
109
|
-
},
|
|
114
|
+
json=body,
|
|
110
115
|
timeout=timeout_s,
|
|
116
|
+
allow_redirects=False,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
token_resp = exchange_runtime_credential(
|
|
121
|
+
_post,
|
|
122
|
+
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,
|
|
129
|
+
deadline=time.monotonic() + timeout_s,
|
|
111
130
|
)
|
|
112
131
|
except Exception:
|
|
113
132
|
return False
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The runtime credential exchange, shared by the auth provider and the logger.
|
|
3
|
+
|
|
4
|
+
A runtime started with ``MAINSEQUENCE_AUTH_MODE=runtime_credential`` exchanges its
|
|
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``.
|
|
13
|
+
|
|
14
|
+
The token exists only inside one exchange request. It is not kept on any object,
|
|
15
|
+
written to the environment or a file, logged, or included in an error message.
|
|
16
|
+
|
|
17
|
+
The logger exchanges a runtime credential while it is being configured, so this
|
|
18
|
+
module imports nothing from the SDK.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import time
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from urllib3.exceptions import InvalidHeader
|
|
29
|
+
from urllib3.util import Retry
|
|
30
|
+
|
|
31
|
+
RUNTIME_CREDENTIAL_ID_ENV = "MAINSEQUENCE_RUNTIME_CREDENTIAL_ID"
|
|
32
|
+
RUNTIME_CREDENTIAL_SECRET_ENV = "MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET"
|
|
33
|
+
RUNTIME_IDENTITY_TOKEN_FILE_ENV = "MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE"
|
|
34
|
+
|
|
35
|
+
# 429: the exchange is throttled. 503: verification is temporarily unavailable.
|
|
36
|
+
# Neither exchanged anything, so the same request may be sent again.
|
|
37
|
+
RETRYABLE_EXCHANGE_STATUSES = frozenset({429, 503})
|
|
38
|
+
EXCHANGE_RETRIES = 3
|
|
39
|
+
EXCHANGE_BACKOFF_FACTOR = 0.5
|
|
40
|
+
|
|
41
|
+
_RETRY_AFTER = Retry(total=0)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RuntimeIdentityTokenError(Exception):
|
|
45
|
+
"""The runtime identity token file cannot be used. The message never holds its content."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _environment_value(name: str) -> str:
|
|
49
|
+
return (os.getenv(name) or "").strip()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def identity_token_file_from_environment() -> str | None:
|
|
53
|
+
"""Return the path in ``MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE``, or None when unset."""
|
|
54
|
+
return _environment_value(RUNTIME_IDENTITY_TOKEN_FILE_ENV) or None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def runtime_credential_configured() -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Say whether the environment holds a runtime credential to exchange.
|
|
60
|
+
|
|
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.
|
|
64
|
+
"""
|
|
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))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _read_identity_token(path: str) -> str:
|
|
73
|
+
source = f"The runtime identity token file {path} ({RUNTIME_IDENTITY_TOKEN_FILE_ENV})"
|
|
74
|
+
try:
|
|
75
|
+
with open(path, "rb") as token_file:
|
|
76
|
+
raw = token_file.read()
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
raise RuntimeIdentityTokenError(f"{source} does not exist.") from None
|
|
79
|
+
except OSError as exc:
|
|
80
|
+
raise RuntimeIdentityTokenError(
|
|
81
|
+
f"{source} could not be read ({type(exc).__name__})."
|
|
82
|
+
) from None
|
|
83
|
+
try:
|
|
84
|
+
token = raw.decode("utf-8").strip()
|
|
85
|
+
except UnicodeDecodeError:
|
|
86
|
+
# Raised below, outside this handler: the decode error holds the bytes.
|
|
87
|
+
token = None
|
|
88
|
+
if token is None:
|
|
89
|
+
raise RuntimeIdentityTokenError(f"{source} does not hold a token.")
|
|
90
|
+
if not token:
|
|
91
|
+
raise RuntimeIdentityTokenError(f"{source} is empty.")
|
|
92
|
+
return token
|
|
93
|
+
|
|
94
|
+
|
|
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 ""}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _retry_delay(response: Any, attempt: int) -> float:
|
|
109
|
+
delay = min(EXCHANGE_BACKOFF_FACTOR * 2**attempt, Retry.DEFAULT_BACKOFF_MAX)
|
|
110
|
+
retry_after = (getattr(response, "headers", None) or {}).get("Retry-After")
|
|
111
|
+
if retry_after is not None:
|
|
112
|
+
try:
|
|
113
|
+
delay = max(delay, _RETRY_AFTER.parse_retry_after(str(retry_after)))
|
|
114
|
+
except InvalidHeader:
|
|
115
|
+
pass
|
|
116
|
+
return delay
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def exchange_runtime_credential(
|
|
120
|
+
send: Callable[[dict[str, str]], Any],
|
|
121
|
+
*,
|
|
122
|
+
credential_id: str,
|
|
123
|
+
credential_secret: str | None,
|
|
124
|
+
identity_token_file: str | None,
|
|
125
|
+
deadline: float,
|
|
126
|
+
on_retry: Callable[[int, float], None] | None = None,
|
|
127
|
+
) -> Any:
|
|
128
|
+
"""
|
|
129
|
+
Send one runtime credential exchange and return the last response.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
send:
|
|
134
|
+
Posts the JSON body it is given to the exchange route and returns the
|
|
135
|
+
response. It is called once per attempt.
|
|
136
|
+
credential_id:
|
|
137
|
+
The runtime credential ID.
|
|
138
|
+
credential_secret:
|
|
139
|
+
The bootstrap secret. Ignored when ``identity_token_file`` is set.
|
|
140
|
+
identity_token_file:
|
|
141
|
+
The projected token file. It is read again for every attempt.
|
|
142
|
+
deadline:
|
|
143
|
+
``time.monotonic()`` value after which no retry starts.
|
|
144
|
+
on_retry:
|
|
145
|
+
Called with the status and the delay before each retry.
|
|
146
|
+
|
|
147
|
+
A ``429`` or ``503`` answer is retried up to ``EXCHANGE_RETRIES`` times. The
|
|
148
|
+
wait is the exponential backoff or the answer's ``Retry-After``, whichever is
|
|
149
|
+
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.
|
|
153
|
+
"""
|
|
154
|
+
for attempt in range(EXCHANGE_RETRIES + 1):
|
|
155
|
+
response = send(_request_body(credential_id, credential_secret, identity_token_file))
|
|
156
|
+
status = response.status_code
|
|
157
|
+
if status not in RETRYABLE_EXCHANGE_STATUSES or attempt == EXCHANGE_RETRIES:
|
|
158
|
+
return response
|
|
159
|
+
delay = _retry_delay(response, attempt)
|
|
160
|
+
if delay >= deadline - time.monotonic():
|
|
161
|
+
return response
|
|
162
|
+
if on_retry is not None:
|
|
163
|
+
on_retry(status, delay)
|
|
164
|
+
close = getattr(response, "close", None)
|
|
165
|
+
if callable(close):
|
|
166
|
+
close()
|
|
167
|
+
time.sleep(delay)
|
|
168
|
+
raise AssertionError("Unreachable retry state")
|
|
@@ -33,6 +33,10 @@ REQUIRED_CLAIMS = frozenset(
|
|
|
33
33
|
"exp",
|
|
34
34
|
}
|
|
35
35
|
)
|
|
36
|
+
# What the platform states about the caller: active team UIDs and the
|
|
37
|
+
# Organization-admin flag. Sent together, or not at all by a platform that
|
|
38
|
+
# predates them; then the caller has no teams and is not an admin.
|
|
39
|
+
CALLER_FACT_CLAIMS = frozenset({"team_uids", "is_organization_admin"})
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
class InvalidCallerAssertion(ValueError):
|
|
@@ -59,6 +63,25 @@ class AuthenticatedCaller:
|
|
|
59
63
|
environment_uid: str
|
|
60
64
|
issued_at: int
|
|
61
65
|
expires_at: int
|
|
66
|
+
team_uids: tuple[str, ...] = ()
|
|
67
|
+
is_organization_admin: bool = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _caller_facts(payload: dict) -> tuple[tuple[str, ...], bool]:
|
|
71
|
+
facts = set(payload) & CALLER_FACT_CLAIMS
|
|
72
|
+
if set(payload) - facts != REQUIRED_CLAIMS or facts not in (set(), set(CALLER_FACT_CLAIMS)):
|
|
73
|
+
raise InvalidCallerAssertion("Caller assertion claims are invalid.")
|
|
74
|
+
if not facts:
|
|
75
|
+
return (), False
|
|
76
|
+
raw_team_uids = payload["team_uids"]
|
|
77
|
+
if not isinstance(raw_team_uids, list):
|
|
78
|
+
raise InvalidCallerAssertion("Caller team UIDs are invalid.")
|
|
79
|
+
team_uids = tuple(_canonical_uid(value) for value in raw_team_uids)
|
|
80
|
+
if list(team_uids) != sorted(set(team_uids)):
|
|
81
|
+
raise InvalidCallerAssertion("Caller team UIDs must be sorted and unique.")
|
|
82
|
+
if type(payload["is_organization_admin"]) is not bool:
|
|
83
|
+
raise InvalidCallerAssertion("Caller admin flag is invalid.")
|
|
84
|
+
return team_uids, payload["is_organization_admin"]
|
|
62
85
|
|
|
63
86
|
|
|
64
87
|
class CallerAssertionVerifier:
|
|
@@ -206,8 +229,7 @@ class CallerAssertionVerifier:
|
|
|
206
229
|
audience=f"urn:mainsequence:fapi:{self.release_uid}",
|
|
207
230
|
options={"require": list(REQUIRED_CLAIMS)},
|
|
208
231
|
)
|
|
209
|
-
|
|
210
|
-
raise InvalidCallerAssertion("Caller assertion claims are invalid.")
|
|
232
|
+
team_uids, is_organization_admin = _caller_facts(payload)
|
|
211
233
|
if (
|
|
212
234
|
payload["aud"] != f"urn:mainsequence:fapi:{self.release_uid}"
|
|
213
235
|
or _canonical_uid(payload["resource_release_uid"]) != self.release_uid
|
|
@@ -226,6 +248,8 @@ class CallerAssertionVerifier:
|
|
|
226
248
|
environment_uid=self.environment_uid,
|
|
227
249
|
issued_at=iat,
|
|
228
250
|
expires_at=exp,
|
|
251
|
+
team_uids=team_uids,
|
|
252
|
+
is_organization_admin=is_organization_admin,
|
|
229
253
|
)
|
|
230
254
|
except (InvalidTokenError, KeyError, TypeError, ValueError) as exc:
|
|
231
255
|
raise InvalidCallerAssertion("Caller assertion is invalid.") from exc
|
|
@@ -213,7 +213,11 @@ class _RequestIdentityMiddleware:
|
|
|
213
213
|
elif self.mode == "assertion":
|
|
214
214
|
assertion = _header(scope, ASSERTION_HEADER.lower().encode()) or ""
|
|
215
215
|
caller = await asyncio.to_thread(self.verifier.verify, assertion)
|
|
216
|
-
user = RequestUserIdentity(
|
|
216
|
+
user = RequestUserIdentity(
|
|
217
|
+
uid=caller.user_uid,
|
|
218
|
+
team_uids=caller.team_uids,
|
|
219
|
+
is_organization_admin=caller.is_organization_admin,
|
|
220
|
+
)
|
|
217
221
|
state["resource_release_uid"] = caller.release_uid
|
|
218
222
|
state["organization_environment_uid"] = caller.environment_uid
|
|
219
223
|
else:
|
|
@@ -20,6 +20,7 @@ mainsequence/code_repository_skills.py
|
|
|
20
20
|
mainsequence/defaults.py
|
|
21
21
|
mainsequence/logconf.py
|
|
22
22
|
mainsequence/repository_identity_security.py
|
|
23
|
+
mainsequence/runtime_credential_exchange.py
|
|
23
24
|
mainsequence/runtime_flags.py
|
|
24
25
|
mainsequence/scaffold_skills.py
|
|
25
26
|
mainsequence.egg-info/PKG-INFO
|
|
@@ -90,6 +91,7 @@ tests/test_logconf.py
|
|
|
90
91
|
tests/test_models_user_request_bound_auth.py
|
|
91
92
|
tests/test_notification_uid_contract.py
|
|
92
93
|
tests/test_observability_client.py
|
|
94
|
+
tests/test_runtime_credential_exchange.py
|
|
93
95
|
tests/test_scaffold_skills.py
|
|
94
96
|
tests/test_secret_client_model.py
|
|
95
97
|
tests/test_source_ownership.py
|