mainsequence 3.18.2__tar.gz → 3.18.4__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-3.18.2 → mainsequence-3.18.4}/PKG-INFO +1 -1
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/api.py +30 -2
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/cli.py +136 -25
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/config.py +5 -4
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/utils.py +117 -1
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/logconf.py +48 -9
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/PKG-INFO +1 -1
- {mainsequence-3.18.2 → mainsequence-3.18.4}/pyproject.toml +1 -1
- mainsequence-3.18.4/tests/test_auth_precedence.py +537 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_cli.py +181 -2
- mainsequence-3.18.2/tests/test_auth_precedence.py +0 -214
- {mainsequence-3.18.2 → mainsequence-3.18.4}/LICENSE +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/README.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/command_center/api_mock_prototyping/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/command_center/app_components/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/command_center/workspace_analysis/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/command_center/workspace_builder/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/dashboards/streamlit/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/data_access/exploration/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/data_publishing/data_nodes/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/data_publishing/simple_tables/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/maintenance/local_journal/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/markets_platform/assets_and_translation/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/markets_platform/instruments_and_pricing/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/markets_platform/virtualfundbuilder/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/agent_scaffold/skills/project_builder/SKILL.md +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/__main__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/bootstrap.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/project_status.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/cli/ui.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/agent.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/base.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/client.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/app_component.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/data_models.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/workspace.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/command_center/workspace_snapshot.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/data_sources_interfaces/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/data_sources_interfaces/duckdb.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/data_sources_interfaces/timescale.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/fastapi/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/fastapi/auth.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_simple_tables.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_tdag.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_user.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/client/models_vam.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/compute_validation.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/config.toml +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_1_base64.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_2_base64.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_3_base64.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_4_base64.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/image_5_base64.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/assets/logo.png +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/components/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/components/asset_select.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/components/date_settings.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/components/logged_user.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/core/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/core/theme.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/instruments/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/instruments/streamlit_form_factory.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/dashboards/streamlit/scaffold.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/data_interface/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/data_interface/data_interface.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/base_instrument.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/bond.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/callability.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/interest_rate_swap.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/json_codec.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/position.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/instruments/ql_fields.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/etl/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/etl/curve_codec.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/etl/nodes.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/interest_rates/etl/registry.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/bond_pricer.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/indices.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/indices_builders.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/pricing_models/swap_pricer.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/settings.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/instruments/utils.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/__main__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/base_persist_managers.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/config.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/configuration_models.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/build_operations.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/data_nodes.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/filters.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/models.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/namespacing.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/persist_managers.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/run_operations.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/data_nodes/utils.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/filters.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/future_registry.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/pydantic_metadata.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/filters.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/models.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/persist_managers.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/schema.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/simple_tables/table_nodes.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/tdag/utils.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/prices/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/prices/utils.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/enums.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/models.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/portfolio_nodes.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence/virtualfundbuilder/utils.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/SOURCES.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/setup.cfg +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_build_operations_hashing.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_client.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_command_center_app_component_models.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_command_center_models.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_dependency_extras.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_filter_normalization.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_instruments.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_logconf.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_logged_user_components.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_pod_project_resolution.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_project_batch_jobs_from_file.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_run_configuration.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_simple_tables_configuration_hashing.py +0 -0
- {mainsequence-3.18.2 → mainsequence-3.18.4}/tests/test_simple_tables_persistence.py +0 -0
|
@@ -178,6 +178,28 @@ def refresh_access() -> str:
|
|
|
178
178
|
NotLoggedIn: if refresh is missing or refresh fails
|
|
179
179
|
"""
|
|
180
180
|
refresh = _refresh_token()
|
|
181
|
+
runtime_mode = (os.environ.get("MAINSEQUENCE_AUTH_MODE") or "").strip().lower() == "runtime_credential"
|
|
182
|
+
|
|
183
|
+
if not refresh and runtime_mode:
|
|
184
|
+
try:
|
|
185
|
+
from mainsequence.client.utils import RuntimeCredentialAuthProvider
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
raise NotLoggedIn(f"Runtime credential auth is unavailable: {exc}") from exc
|
|
188
|
+
|
|
189
|
+
token_url = f"{backend_url().rstrip('/')}/orm/api/pods/runtime-credentials/token/"
|
|
190
|
+
try:
|
|
191
|
+
RuntimeCredentialAuthProvider(token_url=token_url).refresh(force=True)
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
raise NotLoggedIn(f"Runtime credential exchange failed: {exc}") from exc
|
|
194
|
+
|
|
195
|
+
access = (os.environ.get("MAINSEQUENCE_ACCESS_TOKEN") or "").strip()
|
|
196
|
+
if not access:
|
|
197
|
+
raise NotLoggedIn("Runtime credential exchange did not produce MAINSEQUENCE_ACCESS_TOKEN.")
|
|
198
|
+
|
|
199
|
+
tokens = get_tokens()
|
|
200
|
+
save_tokens(tokens.get("username") or "", access, "")
|
|
201
|
+
return access
|
|
202
|
+
|
|
181
203
|
if not refresh:
|
|
182
204
|
raise NotLoggedIn("Not logged in. Run `mainsequence login`.")
|
|
183
205
|
|
|
@@ -422,7 +444,8 @@ def get_logged_user_details() -> dict[str, Any]:
|
|
|
422
444
|
|
|
423
445
|
The CLI does not naturally run inside a request context, so this bridge resolves
|
|
424
446
|
the current user id from the authenticated API session and temporarily binds
|
|
425
|
-
`X-User-ID`
|
|
447
|
+
`X-User-ID` plus `Authorization` into
|
|
448
|
+
`mainsequence.client.models_user._CURRENT_AUTH_HEADERS`
|
|
426
449
|
before calling the SDK method.
|
|
427
450
|
"""
|
|
428
451
|
tokens = get_tokens()
|
|
@@ -486,7 +509,12 @@ def get_logged_user_details() -> dict[str, Any]:
|
|
|
486
509
|
|
|
487
510
|
BaseObjectOrm.ROOT_URL = root_url
|
|
488
511
|
ClientUser.ROOT_URL = root_url
|
|
489
|
-
headers_token = current_auth_headers.set(
|
|
512
|
+
headers_token = current_auth_headers.set(
|
|
513
|
+
{
|
|
514
|
+
"X-User-ID": str(user_id),
|
|
515
|
+
"Authorization": f"Bearer {access}",
|
|
516
|
+
}
|
|
517
|
+
)
|
|
490
518
|
|
|
491
519
|
user = ClientUser.get_logged_user()
|
|
492
520
|
if isinstance(user, dict):
|
|
@@ -226,6 +226,7 @@ from .ssh_utils import (
|
|
|
226
226
|
from .ui import error, info, print_kv, print_table, status, success, warn
|
|
227
227
|
|
|
228
228
|
JSON_OUTPUT_CONTEXT_KEY = "json_output"
|
|
229
|
+
LOGIN_DEFAULT_BACKEND_URL = "https://api.main-sequence.io"
|
|
229
230
|
|
|
230
231
|
|
|
231
232
|
class MainSequenceGroup(typer.core.TyperGroup):
|
|
@@ -756,6 +757,28 @@ def _require_login() -> dict:
|
|
|
756
757
|
raise typer.Exit(1) from e
|
|
757
758
|
|
|
758
759
|
|
|
760
|
+
def _runtime_credential_mode_enabled() -> bool:
|
|
761
|
+
return (os.environ.get("MAINSEQUENCE_AUTH_MODE") or "").strip().lower() == "runtime_credential"
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _exchange_runtime_credential_for_cli_login(backend_url: str) -> str:
|
|
765
|
+
try:
|
|
766
|
+
from mainsequence.client.utils import RuntimeCredentialAuthProvider
|
|
767
|
+
except Exception as exc:
|
|
768
|
+
raise ApiError(f"Runtime credential auth is unavailable: {exc}") from exc
|
|
769
|
+
|
|
770
|
+
token_url = f"{backend_url.rstrip('/')}/orm/api/pods/runtime-credentials/token/"
|
|
771
|
+
try:
|
|
772
|
+
RuntimeCredentialAuthProvider(token_url=token_url).refresh(force=True)
|
|
773
|
+
except Exception as exc:
|
|
774
|
+
raise ApiError(f"Runtime credential exchange failed: {exc}") from exc
|
|
775
|
+
|
|
776
|
+
access = (os.environ.get("MAINSEQUENCE_ACCESS_TOKEN") or "").strip()
|
|
777
|
+
if not access:
|
|
778
|
+
raise ApiError("Runtime credential exchange did not produce MAINSEQUENCE_ACCESS_TOKEN.")
|
|
779
|
+
return access
|
|
780
|
+
|
|
781
|
+
|
|
759
782
|
def _resolve_project_dir(project_id: int | None, path: str | None) -> pathlib.Path:
|
|
760
783
|
"""
|
|
761
784
|
Resolve project directory by:
|
|
@@ -2034,10 +2057,15 @@ def login(
|
|
|
2034
2057
|
Persists auth tokens in the active CLI auth store so subsequent
|
|
2035
2058
|
CLI invocations can run without re-authentication. Backend/base-folder
|
|
2036
2059
|
overrides passed to `login` are scoped to the current terminal session.
|
|
2060
|
+
When no backend is provided, login defaults to `https://api.main-sequence.io`.
|
|
2037
2061
|
|
|
2038
2062
|
Interactive login uses browser-based authentication and finishes with
|
|
2039
2063
|
standard JWT access/refresh tokens persisted by the CLI.
|
|
2040
2064
|
|
|
2065
|
+
If `MAINSEQUENCE_AUTH_MODE=runtime_credential`, login exchanges the
|
|
2066
|
+
configured runtime credential for a short-lived access token instead of
|
|
2067
|
+
opening the browser or persisting CLI JWT tokens.
|
|
2068
|
+
|
|
2041
2069
|
Parameters
|
|
2042
2070
|
----------
|
|
2043
2071
|
backend:
|
|
@@ -2066,9 +2094,21 @@ def login(
|
|
|
2066
2094
|
mainsequence login --access-token "$TOKEN" --refresh-token "$REFRESH"
|
|
2067
2095
|
mainsequence login --access-token "$TOKEN" --refresh-token "$REFRESH" --backend http://127.0.0.1:8000 --projects-base mainsequence-dev
|
|
2068
2096
|
mainsequence login --export
|
|
2097
|
+
MAINSEQUENCE_AUTH_MODE=runtime_credential mainsequence login
|
|
2069
2098
|
```
|
|
2070
2099
|
"""
|
|
2071
2100
|
using_jwt = bool((access_token or "").strip() or (refresh_token or "").strip())
|
|
2101
|
+
using_runtime_credential = _runtime_credential_mode_enabled()
|
|
2102
|
+
|
|
2103
|
+
if using_runtime_credential and using_jwt:
|
|
2104
|
+
error(
|
|
2105
|
+
"Runtime credential login cannot be combined with "
|
|
2106
|
+
"--access-token/--refresh-token."
|
|
2107
|
+
)
|
|
2108
|
+
raise typer.Exit(1)
|
|
2109
|
+
|
|
2110
|
+
if using_runtime_credential and no_open:
|
|
2111
|
+
warn("--no-open is ignored when MAINSEQUENCE_AUTH_MODE=runtime_credential.")
|
|
2072
2112
|
|
|
2073
2113
|
if not using_jwt and backend and "@" in backend:
|
|
2074
2114
|
error(
|
|
@@ -2081,7 +2121,8 @@ def login(
|
|
|
2081
2121
|
if cfg.normalize_backend_url(backend) != cfg.normalize_backend_url(backend_option):
|
|
2082
2122
|
error("Pass backend either positionally or with --backend, not both.")
|
|
2083
2123
|
raise typer.Exit(1)
|
|
2084
|
-
|
|
2124
|
+
explicit_backend_input = backend_option if backend_option is not None else backend
|
|
2125
|
+
effective_backend_input = explicit_backend_input if explicit_backend_input is not None else LOGIN_DEFAULT_BACKEND_URL
|
|
2085
2126
|
|
|
2086
2127
|
if projects_base and projects_base_option:
|
|
2087
2128
|
if cfg.normalize_mainsequence_path(projects_base) != cfg.normalize_mainsequence_path(projects_base_option):
|
|
@@ -2100,28 +2141,39 @@ def login(
|
|
|
2100
2141
|
raise typer.Exit(1)
|
|
2101
2142
|
|
|
2102
2143
|
current_backend = cfg.backend_url()
|
|
2103
|
-
normalized_backend = cfg.normalize_backend_url(effective_backend_input)
|
|
2144
|
+
normalized_backend = cfg.normalize_backend_url(effective_backend_input)
|
|
2104
2145
|
|
|
2105
|
-
if
|
|
2146
|
+
if explicit_backend_input is not None and normalized_backend != current_backend:
|
|
2106
2147
|
if not effective_projects_base_input:
|
|
2107
2148
|
error("When using a different backend, you must also specify a projects base folder.")
|
|
2108
2149
|
raise typer.Exit(1)
|
|
2109
2150
|
|
|
2110
2151
|
previous_backend_override = os.environ.get("MAIN_SEQUENCE_BACKEND_URL")
|
|
2111
|
-
|
|
2112
|
-
os.environ["MAIN_SEQUENCE_BACKEND_URL"] = normalized_backend
|
|
2152
|
+
os.environ["MAIN_SEQUENCE_BACKEND_URL"] = normalized_backend
|
|
2113
2153
|
|
|
2114
2154
|
try:
|
|
2115
|
-
if
|
|
2155
|
+
if using_runtime_credential:
|
|
2156
|
+
access = _exchange_runtime_credential_for_cli_login(normalized_backend)
|
|
2157
|
+
persisted = cfg.save_tokens("", access, "")
|
|
2158
|
+
res = {
|
|
2159
|
+
"username": "",
|
|
2160
|
+
"backend": normalized_backend,
|
|
2161
|
+
"access": access,
|
|
2162
|
+
"refresh": "",
|
|
2163
|
+
"persisted": bool(persisted),
|
|
2164
|
+
"auth_mode": "runtime_credential",
|
|
2165
|
+
}
|
|
2166
|
+
elif using_jwt:
|
|
2116
2167
|
os.environ.pop(cfg.ENV_USERNAME, None)
|
|
2117
2168
|
os.environ.pop(cfg.LEGACY_ENV_USERNAME, None)
|
|
2118
2169
|
persisted = cfg.save_tokens("", (access_token or "").strip(), (refresh_token or "").strip())
|
|
2119
2170
|
res = {
|
|
2120
2171
|
"username": "",
|
|
2121
|
-
"backend": normalized_backend
|
|
2172
|
+
"backend": normalized_backend,
|
|
2122
2173
|
"access": (access_token or "").strip(),
|
|
2123
2174
|
"refresh": (refresh_token or "").strip(),
|
|
2124
2175
|
"persisted": bool(persisted),
|
|
2176
|
+
"auth_mode": "jwt",
|
|
2125
2177
|
}
|
|
2126
2178
|
else:
|
|
2127
2179
|
def _emit_auth_url(url: str) -> None:
|
|
@@ -2146,10 +2198,11 @@ def login(
|
|
|
2146
2198
|
|
|
2147
2199
|
res = {
|
|
2148
2200
|
"username": username,
|
|
2149
|
-
"backend": normalized_backend
|
|
2201
|
+
"backend": normalized_backend,
|
|
2150
2202
|
"access": access,
|
|
2151
2203
|
"refresh": refresh,
|
|
2152
2204
|
"persisted": bool(persisted),
|
|
2205
|
+
"auth_mode": "jwt",
|
|
2153
2206
|
}
|
|
2154
2207
|
except BrowserAuthError as e:
|
|
2155
2208
|
error(f"Browser login failed: {e}")
|
|
@@ -2158,26 +2211,26 @@ def login(
|
|
|
2158
2211
|
error(f"Login failed: {e}")
|
|
2159
2212
|
raise typer.Exit(1) from e
|
|
2160
2213
|
finally:
|
|
2161
|
-
if
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
os.environ["MAIN_SEQUENCE_BACKEND_URL"] = previous_backend_override
|
|
2214
|
+
if previous_backend_override is None:
|
|
2215
|
+
os.environ.pop("MAIN_SEQUENCE_BACKEND_URL", None)
|
|
2216
|
+
else:
|
|
2217
|
+
os.environ["MAIN_SEQUENCE_BACKEND_URL"] = previous_backend_override
|
|
2166
2218
|
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
)
|
|
2172
|
-
else:
|
|
2173
|
-
cfg.clear_session_overrides()
|
|
2219
|
+
cfg.set_session_overrides(
|
|
2220
|
+
backend_url=normalized_backend,
|
|
2221
|
+
mainsequence_path=effective_projects_base_input,
|
|
2222
|
+
)
|
|
2174
2223
|
|
|
2175
2224
|
if export:
|
|
2176
2225
|
access = (res.get("access") or "").replace('"', '\\"')
|
|
2177
2226
|
refresh = (res.get("refresh") or "").replace('"', '\\"')
|
|
2178
2227
|
username = (res.get("username") or "").replace('"', '\\"')
|
|
2228
|
+
auth_mode = (res.get("auth_mode") or "").replace('"', '\\"')
|
|
2229
|
+
if auth_mode:
|
|
2230
|
+
typer.echo(f'export MAINSEQUENCE_AUTH_MODE="{auth_mode}"')
|
|
2179
2231
|
typer.echo(f'export MAINSEQUENCE_ACCESS_TOKEN="{access}"')
|
|
2180
|
-
|
|
2232
|
+
if refresh:
|
|
2233
|
+
typer.echo(f'export MAINSEQUENCE_REFRESH_TOKEN="{refresh}"')
|
|
2181
2234
|
if username:
|
|
2182
2235
|
typer.echo(f'export MAINSEQUENCE_USERNAME="{username}"')
|
|
2183
2236
|
return
|
|
@@ -2188,11 +2241,16 @@ def login(
|
|
|
2188
2241
|
typer.echo("MAIN SEQUENCE")
|
|
2189
2242
|
if res.get("username"):
|
|
2190
2243
|
success(f"Signed in as {res['username']} (Backend: {res['backend']})")
|
|
2244
|
+
elif res.get("auth_mode") == "runtime_credential":
|
|
2245
|
+
success(f"Signed in with runtime credential (Backend: {res['backend']})")
|
|
2191
2246
|
else:
|
|
2192
2247
|
success(f"Signed in with JWT tokens (Backend: {res['backend']})")
|
|
2193
2248
|
info(f"Projects base folder: {base}")
|
|
2194
2249
|
auth_store_label = cfg.auth_persistence_label()
|
|
2195
|
-
if res.get("
|
|
2250
|
+
if res.get("auth_mode") == "runtime_credential":
|
|
2251
|
+
info(f"Runtime credential access token is persisted in {auth_store_label}; no CLI JWT refresh token exists.")
|
|
2252
|
+
info("When the access token expires, CLI will re-exchange the runtime credential automatically.")
|
|
2253
|
+
elif res.get("persisted", True):
|
|
2196
2254
|
info(f"Auth tokens are persisted in {auth_store_label} for subsequent CLI commands.")
|
|
2197
2255
|
else:
|
|
2198
2256
|
warn(f"Could not persist auth tokens in {auth_store_label}. Use --export for shell-based auth.")
|
|
@@ -2815,7 +2873,7 @@ def settings_set_base(path: str = typer.Argument(..., help="New projects base fo
|
|
|
2815
2873
|
|
|
2816
2874
|
@settings.command("set-backend")
|
|
2817
2875
|
def settings_set_backend(
|
|
2818
|
-
url: str = typer.Argument(..., help="Backend base URL, e.g. https://api.main-sequence.
|
|
2876
|
+
url: str = typer.Argument(..., help="Backend base URL, e.g. https://api.main-sequence.io")
|
|
2819
2877
|
):
|
|
2820
2878
|
"""
|
|
2821
2879
|
Set backend base URL used by CLI API calls.
|
|
@@ -2823,12 +2881,12 @@ def settings_set_backend(
|
|
|
2823
2881
|
Parameters
|
|
2824
2882
|
----------
|
|
2825
2883
|
url:
|
|
2826
|
-
Backend base URL (for example `https://api.main-sequence.
|
|
2884
|
+
Backend base URL (for example `https://api.main-sequence.io`).
|
|
2827
2885
|
|
|
2828
2886
|
Examples
|
|
2829
2887
|
--------
|
|
2830
2888
|
```bash
|
|
2831
|
-
mainsequence settings set-backend https://api.main-sequence.
|
|
2889
|
+
mainsequence settings set-backend https://api.main-sequence.io
|
|
2832
2890
|
```
|
|
2833
2891
|
"""
|
|
2834
2892
|
out = cfg.set_backend_url(url)
|
|
@@ -2837,6 +2895,59 @@ def settings_set_backend(
|
|
|
2837
2895
|
success(f"Backend URL set to: {out.get('backend_url')}")
|
|
2838
2896
|
|
|
2839
2897
|
|
|
2898
|
+
def _settings_reset_impl() -> dict:
|
|
2899
|
+
"""
|
|
2900
|
+
Reset persistent CLI settings to standard defaults and clear session overrides.
|
|
2901
|
+
"""
|
|
2902
|
+
standard_backend = cfg.normalize_backend_url(LOGIN_DEFAULT_BACKEND_URL)
|
|
2903
|
+
standard_base = cfg.normalize_mainsequence_path(cfg.DEFAULTS.get("mainsequence_path"))
|
|
2904
|
+
pathlib.Path(standard_base).mkdir(parents=True, exist_ok=True)
|
|
2905
|
+
out = cfg.set_config(
|
|
2906
|
+
{
|
|
2907
|
+
"backend_url": standard_backend,
|
|
2908
|
+
"mainsequence_path": standard_base,
|
|
2909
|
+
}
|
|
2910
|
+
)
|
|
2911
|
+
cfg.clear_session_overrides()
|
|
2912
|
+
return out
|
|
2913
|
+
|
|
2914
|
+
|
|
2915
|
+
@settings.command("reset")
|
|
2916
|
+
def settings_reset():
|
|
2917
|
+
"""
|
|
2918
|
+
Reset CLI settings to standard defaults.
|
|
2919
|
+
|
|
2920
|
+
Resets backend URL to `https://api.main-sequence.io`, base folder to the
|
|
2921
|
+
default `~/mainsequence`, and clears current terminal session overrides.
|
|
2922
|
+
|
|
2923
|
+
Examples
|
|
2924
|
+
--------
|
|
2925
|
+
```bash
|
|
2926
|
+
mainsequence settings reset
|
|
2927
|
+
```
|
|
2928
|
+
"""
|
|
2929
|
+
out = _settings_reset_impl()
|
|
2930
|
+
if _emit_json(out):
|
|
2931
|
+
return
|
|
2932
|
+
success("Settings reset to standard defaults.")
|
|
2933
|
+
info(f"Backend URL: {out.get('backend_url')}")
|
|
2934
|
+
info(f"Projects base folder: {out.get('mainsequence_path')}")
|
|
2935
|
+
|
|
2936
|
+
|
|
2937
|
+
@settings.command("refresh")
|
|
2938
|
+
def settings_refresh():
|
|
2939
|
+
"""
|
|
2940
|
+
Alias for `settings reset`.
|
|
2941
|
+
|
|
2942
|
+
Examples
|
|
2943
|
+
--------
|
|
2944
|
+
```bash
|
|
2945
|
+
mainsequence settings refresh
|
|
2946
|
+
```
|
|
2947
|
+
"""
|
|
2948
|
+
settings_reset()
|
|
2949
|
+
|
|
2950
|
+
|
|
2840
2951
|
# ---------- sdk group ----------
|
|
2841
2952
|
|
|
2842
2953
|
|
|
@@ -62,7 +62,7 @@ KEYCHAIN_SERVICE = "MainSequenceCLI.auth"
|
|
|
62
62
|
KEYCHAIN_ACCOUNT = "default"
|
|
63
63
|
|
|
64
64
|
DEFAULTS = {
|
|
65
|
-
"backend_url": os.environ.get("MAIN_SEQUENCE_BACKEND_URL", "https://api.main-sequence.
|
|
65
|
+
"backend_url": os.environ.get("MAIN_SEQUENCE_BACKEND_URL", "https://api.main-sequence.io/"),
|
|
66
66
|
"mainsequence_path": str(pathlib.Path.home() / "mainsequence"),
|
|
67
67
|
"version": 1,
|
|
68
68
|
}
|
|
@@ -212,7 +212,7 @@ def set_backend_url(url: str) -> dict:
|
|
|
212
212
|
Convenience helper to set backend_url in config.json.
|
|
213
213
|
|
|
214
214
|
Args:
|
|
215
|
-
url: Backend base URL (e.g. https://api.main-sequence.
|
|
215
|
+
url: Backend base URL (e.g. https://api.main-sequence.io)
|
|
216
216
|
|
|
217
217
|
Returns:
|
|
218
218
|
dict: updated config
|
|
@@ -350,12 +350,13 @@ def get_tokens() -> dict:
|
|
|
350
350
|
"""
|
|
351
351
|
Return auth tokens from environment variables, with persistent-store fallback.
|
|
352
352
|
"""
|
|
353
|
+
runtime_mode = (os.environ.get("MAINSEQUENCE_AUTH_MODE") or "").strip().lower() == "runtime_credential"
|
|
353
354
|
tokens = {
|
|
354
355
|
"username": os.environ.get(ENV_USERNAME) or os.environ.get(LEGACY_ENV_USERNAME, ""),
|
|
355
356
|
"access": os.environ.get(ENV_ACCESS) or os.environ.get(LEGACY_ENV_ACCESS, ""),
|
|
356
357
|
"refresh": os.environ.get(ENV_REFRESH) or os.environ.get(LEGACY_ENV_REFRESH, ""),
|
|
357
358
|
}
|
|
358
|
-
if tokens["access"] and tokens["refresh"]:
|
|
359
|
+
if tokens["access"] and (tokens["refresh"] or runtime_mode):
|
|
359
360
|
return tokens
|
|
360
361
|
|
|
361
362
|
for secret in (_read_secure_tokens(), _read_local_tokens()):
|
|
@@ -366,7 +367,7 @@ def get_tokens() -> dict:
|
|
|
366
367
|
"access": tokens["access"] or secret.get("access", ""),
|
|
367
368
|
"refresh": tokens["refresh"] or secret.get("refresh", ""),
|
|
368
369
|
}
|
|
369
|
-
if tokens["access"] and tokens["refresh"]:
|
|
370
|
+
if tokens["access"] and (tokens["refresh"] or runtime_mode):
|
|
370
371
|
break
|
|
371
372
|
return tokens
|
|
372
373
|
|
|
@@ -86,6 +86,9 @@ def _default_auth_provider_kind() -> str | None:
|
|
|
86
86
|
has_access = _env_has_value("MAINSEQUENCE_ACCESS_TOKEN")
|
|
87
87
|
has_refresh = _env_has_value("MAINSEQUENCE_REFRESH_TOKEN")
|
|
88
88
|
|
|
89
|
+
if mode == "runtime_credential":
|
|
90
|
+
return "runtime_credential"
|
|
91
|
+
|
|
89
92
|
if mode == "session_jwt":
|
|
90
93
|
if has_access or has_refresh:
|
|
91
94
|
return "session_jwt"
|
|
@@ -176,6 +179,114 @@ class SessionJWTAuthProvider(BaseAuthProvider):
|
|
|
176
179
|
return None
|
|
177
180
|
|
|
178
181
|
|
|
182
|
+
@dataclass
|
|
183
|
+
class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
184
|
+
credential_id: str | None = None
|
|
185
|
+
credential_secret: str | None = None
|
|
186
|
+
token_url: str = f"{API_ENDPOINT}/pods/runtime-credentials/token/"
|
|
187
|
+
token_type: str = "Bearer"
|
|
188
|
+
refresh_skew_seconds: int = 30
|
|
189
|
+
timeout: tuple[float, float] = DEFAULT_TIMEOUT
|
|
190
|
+
expires_at: float | None = None
|
|
191
|
+
_lock: threading.RLock = field(default_factory=threading.RLock, init=False, repr=False)
|
|
192
|
+
|
|
193
|
+
def __post_init__(self):
|
|
194
|
+
if self.credential_id is None:
|
|
195
|
+
self.credential_id = os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID")
|
|
196
|
+
if self.credential_secret is None:
|
|
197
|
+
self.credential_secret = os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET")
|
|
198
|
+
|
|
199
|
+
def _current_access_token(self) -> str | None:
|
|
200
|
+
return (os.getenv("MAINSEQUENCE_ACCESS_TOKEN") or "").strip() or None
|
|
201
|
+
|
|
202
|
+
def _needs_exchange(self) -> bool:
|
|
203
|
+
access_token = self._current_access_token()
|
|
204
|
+
if not access_token:
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
if self.expires_at is not None:
|
|
208
|
+
return self.expires_at <= time.time() + self.refresh_skew_seconds
|
|
209
|
+
|
|
210
|
+
exp = _decode_jwt_exp(access_token)
|
|
211
|
+
if exp is None:
|
|
212
|
+
# Access-only JWT behavior: use opaque/uninspectable access until a 401 forces exchange.
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
return exp <= int(time.time()) + self.refresh_skew_seconds
|
|
216
|
+
|
|
217
|
+
def _require_credentials(self) -> tuple[str, str]:
|
|
218
|
+
credential_id = (self.credential_id or "").strip()
|
|
219
|
+
credential_secret = (self.credential_secret or "").strip()
|
|
220
|
+
if not credential_id:
|
|
221
|
+
raise AuthError(
|
|
222
|
+
"MAINSEQUENCE_RUNTIME_CREDENTIAL_ID is required when "
|
|
223
|
+
"MAINSEQUENCE_AUTH_MODE=runtime_credential."
|
|
224
|
+
)
|
|
225
|
+
if not credential_secret:
|
|
226
|
+
raise AuthError(
|
|
227
|
+
"MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET is required when "
|
|
228
|
+
"MAINSEQUENCE_AUTH_MODE=runtime_credential."
|
|
229
|
+
)
|
|
230
|
+
return credential_id, credential_secret
|
|
231
|
+
|
|
232
|
+
def refresh(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
force: bool = False,
|
|
236
|
+
session: requests.Session | None = None,
|
|
237
|
+
) -> None:
|
|
238
|
+
_ = session
|
|
239
|
+
with self._lock:
|
|
240
|
+
if not force and not self._needs_exchange():
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
credential_id, credential_secret = self._require_credentials()
|
|
244
|
+
response = requests.post(
|
|
245
|
+
self.token_url,
|
|
246
|
+
json={
|
|
247
|
+
"credential_id": credential_id,
|
|
248
|
+
"credential_secret": credential_secret,
|
|
249
|
+
},
|
|
250
|
+
headers={"Content-Type": "application/json"},
|
|
251
|
+
timeout=self.timeout,
|
|
252
|
+
)
|
|
253
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
254
|
+
raise AuthError(
|
|
255
|
+
"Runtime credential exchange failed with status "
|
|
256
|
+
f"{response.status_code}."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
data = response.json()
|
|
260
|
+
access = str(data.get("access") or "").strip()
|
|
261
|
+
if not access:
|
|
262
|
+
raise AuthError("Runtime credential exchange response did not include access token.")
|
|
263
|
+
|
|
264
|
+
token_type = str(data.get("token_type") or self.token_type or "Bearer").strip()
|
|
265
|
+
self.token_type = token_type or "Bearer"
|
|
266
|
+
|
|
267
|
+
expires_in_raw = data.get("expires_in")
|
|
268
|
+
try:
|
|
269
|
+
expires_in = int(expires_in_raw)
|
|
270
|
+
except (TypeError, ValueError):
|
|
271
|
+
expires_in = None
|
|
272
|
+
self.expires_at = time.time() + expires_in if expires_in and expires_in > 0 else None
|
|
273
|
+
os.environ["MAINSEQUENCE_ACCESS_TOKEN"] = access
|
|
274
|
+
|
|
275
|
+
def get_headers(self) -> CaseInsensitiveDict:
|
|
276
|
+
if self._needs_exchange():
|
|
277
|
+
self.refresh(force=False)
|
|
278
|
+
|
|
279
|
+
access_token = self._current_access_token()
|
|
280
|
+
if not access_token:
|
|
281
|
+
raise AuthError("MAINSEQUENCE_ACCESS_TOKEN is missing after runtime credential exchange.")
|
|
282
|
+
|
|
283
|
+
return CaseInsensitiveDict(
|
|
284
|
+
{
|
|
285
|
+
"Authorization": f"{self.token_type} {access_token}",
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
179
290
|
@dataclass
|
|
180
291
|
class JWTAuthProvider(BaseAuthProvider):
|
|
181
292
|
access_token: str | None = None
|
|
@@ -310,6 +421,9 @@ def build_default_auth_provider() -> BaseAuthProvider:
|
|
|
310
421
|
if provider_kind == "session_jwt":
|
|
311
422
|
return SessionJWTAuthProvider()
|
|
312
423
|
|
|
424
|
+
if provider_kind == "runtime_credential":
|
|
425
|
+
return RuntimeCredentialAuthProvider()
|
|
426
|
+
|
|
313
427
|
if provider_kind == "jwt":
|
|
314
428
|
return JWTAuthProvider()
|
|
315
429
|
|
|
@@ -330,7 +444,9 @@ class AuthLoaders:
|
|
|
330
444
|
def _provider(self) -> BaseAuthProvider:
|
|
331
445
|
provider_kind = _default_auth_provider_kind()
|
|
332
446
|
|
|
333
|
-
if provider_kind == "
|
|
447
|
+
if provider_kind == "runtime_credential" and not isinstance(self.provider, RuntimeCredentialAuthProvider):
|
|
448
|
+
self.provider = RuntimeCredentialAuthProvider()
|
|
449
|
+
elif provider_kind == "session_jwt" and not isinstance(self.provider, SessionJWTAuthProvider):
|
|
334
450
|
self.provider = SessionJWTAuthProvider()
|
|
335
451
|
elif provider_kind == "jwt" and not isinstance(self.provider, JWTAuthProvider):
|
|
336
452
|
self.provider = JWTAuthProvider()
|
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
import logging
|
|
4
5
|
import logging.config
|
|
5
6
|
import os
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
from collections.abc import Mapping
|
|
6
10
|
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
7
12
|
|
|
8
13
|
import requests
|
|
9
14
|
import structlog
|
|
10
15
|
from requests.structures import CaseInsensitiveDict
|
|
16
|
+
from structlog.contextvars import bind_contextvars, unbind_contextvars
|
|
11
17
|
from structlog.dev import ConsoleRenderer
|
|
18
|
+
from structlog.stdlib import BoundLogger
|
|
12
19
|
|
|
13
20
|
from .instrumentation import OTelJSONRenderer
|
|
14
21
|
from .runtime_flags import is_running_in_pod
|
|
15
22
|
|
|
16
23
|
logger = None
|
|
17
|
-
import inspect
|
|
18
|
-
import sys
|
|
19
|
-
import traceback
|
|
20
|
-
from collections.abc import Mapping
|
|
21
|
-
from typing import Any
|
|
22
|
-
|
|
23
|
-
from structlog.contextvars import bind_contextvars, unbind_contextvars
|
|
24
|
-
from structlog.stdlib import BoundLogger
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def ensure_dir(file_path):
|
|
@@ -85,7 +84,44 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
|
|
|
85
84
|
|
|
86
85
|
return headers, False
|
|
87
86
|
|
|
87
|
+
def _exchange_runtime_credential() -> bool:
|
|
88
|
+
credential_id = (os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID") or "").strip()
|
|
89
|
+
credential_secret = (os.getenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET") or "").strip()
|
|
90
|
+
if not credential_id or not credential_secret:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
token_resp = requests.post(
|
|
95
|
+
f"{_backend_base_url()}/orm/api/pods/runtime-credentials/token/",
|
|
96
|
+
headers={"Content-Type": "application/json"},
|
|
97
|
+
json={
|
|
98
|
+
"credential_id": credential_id,
|
|
99
|
+
"credential_secret": credential_secret,
|
|
100
|
+
},
|
|
101
|
+
timeout=timeout_s,
|
|
102
|
+
)
|
|
103
|
+
except Exception:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
if token_resp.status_code < 200 or token_resp.status_code >= 300:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
data = token_resp.json()
|
|
111
|
+
except Exception:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
access_token = str(data.get("access") or "").strip()
|
|
115
|
+
if not access_token:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
os.environ["MAINSEQUENCE_ACCESS_TOKEN"] = access_token
|
|
119
|
+
return True
|
|
120
|
+
|
|
88
121
|
def _refresh_access_token() -> bool:
|
|
122
|
+
if auth_mode == "runtime_credential":
|
|
123
|
+
return _exchange_runtime_credential()
|
|
124
|
+
|
|
89
125
|
if auth_mode == "session_jwt":
|
|
90
126
|
return False
|
|
91
127
|
|
|
@@ -127,12 +163,15 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
|
|
|
127
163
|
)
|
|
128
164
|
|
|
129
165
|
if (
|
|
130
|
-
auth_mode
|
|
166
|
+
auth_mode == "jwt"
|
|
131
167
|
and not os.getenv("MAINSEQUENCE_ACCESS_TOKEN")
|
|
132
168
|
and os.getenv("MAINSEQUENCE_REFRESH_TOKEN")
|
|
133
169
|
):
|
|
134
170
|
_refresh_access_token()
|
|
135
171
|
|
|
172
|
+
if auth_mode == "runtime_credential" and not os.getenv("MAINSEQUENCE_ACCESS_TOKEN"):
|
|
173
|
+
_refresh_access_token()
|
|
174
|
+
|
|
136
175
|
headers, using_jwt = _auth_headers()
|
|
137
176
|
|
|
138
177
|
job_run_id = (os.getenv("JOB_RUN_ID") or "").strip()
|