mostlyright-data 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mostlyright/data_harness/__init__.py +158 -0
- mostlyright/data_harness/acquisition/__init__.py +55 -0
- mostlyright/data_harness/acquisition/http.py +2773 -0
- mostlyright/data_harness/acquisition/parsing.py +809 -0
- mostlyright/data_harness/acquisition/ranges.py +495 -0
- mostlyright/data_harness/acquisition/result_download.py +360 -0
- mostlyright/data_harness/acquisition/retention_admission.py +248 -0
- mostlyright/data_harness/acquisition/sandbox.py +4888 -0
- mostlyright/data_harness/acquisition/url_policy.py +530 -0
- mostlyright/data_harness/agent_runtime.py +2743 -0
- mostlyright/data_harness/assets/logo-ink.svg +31 -0
- mostlyright/data_harness/backends/__init__.py +28 -0
- mostlyright/data_harness/backends/pandas_backend.py +350 -0
- mostlyright/data_harness/backends/polars_backend.py +366 -0
- mostlyright/data_harness/backends/protocol.py +124 -0
- mostlyright/data_harness/backends/reference.py +83 -0
- mostlyright/data_harness/backends/registry.py +55 -0
- mostlyright/data_harness/backends/restrictions.py +126 -0
- mostlyright/data_harness/canonical.py +333 -0
- mostlyright/data_harness/catalog_job.py +625 -0
- mostlyright/data_harness/cli.py +5398 -0
- mostlyright/data_harness/contracts.py +53 -0
- mostlyright/data_harness/coordinator.py +1307 -0
- mostlyright/data_harness/deploy.py +924 -0
- mostlyright/data_harness/deploy_target.py +312 -0
- mostlyright/data_harness/deployment_evidence.py +1067 -0
- mostlyright/data_harness/event_presentation.py +576 -0
- mostlyright/data_harness/events.py +2152 -0
- mostlyright/data_harness/fast_delimited.py +239 -0
- mostlyright/data_harness/fleet.py +237 -0
- mostlyright/data_harness/formats.py +236 -0
- mostlyright/data_harness/governors.py +1163 -0
- mostlyright/data_harness/hosted_bootstrap.py +972 -0
- mostlyright/data_harness/hosted_crawler.py +1115 -0
- mostlyright/data_harness/hosted_crawler_container_smoke.py +351 -0
- mostlyright/data_harness/hosted_crawler_fetch.py +423 -0
- mostlyright/data_harness/hosted_crawler_job.py +1277 -0
- mostlyright/data_harness/hosted_crawler_protocol.py +676 -0
- mostlyright/data_harness/hosted_dataset.py +1500 -0
- mostlyright/data_harness/hosted_deploy.py +3037 -0
- mostlyright/data_harness/hosted_handoff.py +62 -0
- mostlyright/data_harness/hosted_ingestion_contract.py +504 -0
- mostlyright/data_harness/hosted_ingestion_job.py +356 -0
- mostlyright/data_harness/hosted_ingestion_job_smoke.py +40 -0
- mostlyright/data_harness/hosted_session_container_smoke.py +194 -0
- mostlyright/data_harness/hosted_session_worker.py +3554 -0
- mostlyright/data_harness/hosted_session_worker_job_smoke.py +46 -0
- mostlyright/data_harness/hosted_worker.py +6784 -0
- mostlyright/data_harness/ingestion/__init__.py +56 -0
- mostlyright/data_harness/ingestion/contracts.py +461 -0
- mostlyright/data_harness/ingestion/faults.py +42 -0
- mostlyright/data_harness/ingestion/gcs_store.py +1162 -0
- mostlyright/data_harness/ingestion/spool.py +130 -0
- mostlyright/data_harness/ingestion/store.py +885 -0
- mostlyright/data_harness/key_seam.py +434 -0
- mostlyright/data_harness/linux_process_boundary.py +262 -0
- mostlyright/data_harness/local_contracts.py +2880 -0
- mostlyright/data_harness/local_search/__init__.py +5 -0
- mostlyright/data_harness/local_search/build_index.py +1087 -0
- mostlyright/data_harness/local_search/contracts.py +920 -0
- mostlyright/data_harness/local_search/query_trace.py +266 -0
- mostlyright/data_harness/local_search/retrieval.py +700 -0
- mostlyright/data_harness/local_search/sealed.py +474 -0
- mostlyright/data_harness/local_search/service.py +784 -0
- mostlyright/data_harness/nbrender/CONTRACT.md +212 -0
- mostlyright/data_harness/nbrender/__init__.py +12 -0
- mostlyright/data_harness/nbrender/chrome.py +359 -0
- mostlyright/data_harness/nbrender/code_body.py +266 -0
- mostlyright/data_harness/nbrender/document.py +407 -0
- mostlyright/data_harness/nbrender/frame.py +275 -0
- mostlyright/data_harness/nbrender/interactive.py +337 -0
- mostlyright/data_harness/nbrender/markdown_body.py +477 -0
- mostlyright/data_harness/nbrender/mr_components.py +134 -0
- mostlyright/data_harness/nbrender/outputs_data.py +595 -0
- mostlyright/data_harness/nbrender/outputs_rich.py +906 -0
- mostlyright/data_harness/nbrender/outputs_source.py +260 -0
- mostlyright/data_harness/nbrender/outputs_stage.py +176 -0
- mostlyright/data_harness/nbrender/outputs_text.py +400 -0
- mostlyright/data_harness/nbrender/parse.py +394 -0
- mostlyright/data_harness/nbrender/status.py +40 -0
- mostlyright/data_harness/nbrender/tokens.py +1295 -0
- mostlyright/data_harness/notebook.py +1710 -0
- mostlyright/data_harness/offline.py +2049 -0
- mostlyright/data_harness/operation_registry.py +1007 -0
- mostlyright/data_harness/operator_setup.py +239 -0
- mostlyright/data_harness/pipeline.py +6428 -0
- mostlyright/data_harness/plan_graph.py +2026 -0
- mostlyright/data_harness/preparation/__init__.py +104 -0
- mostlyright/data_harness/preparation/contracts.py +1017 -0
- mostlyright/data_harness/preparation/engine.py +221 -0
- mostlyright/data_harness/preparation/errors.py +14 -0
- mostlyright/data_harness/preparation/gates.py +751 -0
- mostlyright/data_harness/preparation/joins.py +574 -0
- mostlyright/data_harness/preparation/profile.py +384 -0
- mostlyright/data_harness/preparation/table.py +217 -0
- mostlyright/data_harness/preparation/transforms.py +568 -0
- mostlyright/data_harness/progress_events.py +534 -0
- mostlyright/data_harness/readers/__init__.py +46 -0
- mostlyright/data_harness/readers/containers.py +963 -0
- mostlyright/data_harness/readers/contracts.py +542 -0
- mostlyright/data_harness/readers/delimited.py +257 -0
- mostlyright/data_harness/readers/grib2/__init__.py +33 -0
- mostlyright/data_harness/readers/grib2/admission.py +722 -0
- mostlyright/data_harness/readers/grib2/decode.py +1009 -0
- mostlyright/data_harness/readers/grib2/geometry.py +1133 -0
- mostlyright/data_harness/readers/grib2/portable_math.py +501 -0
- mostlyright/data_harness/readers/json_tabular.py +485 -0
- mostlyright/data_harness/readers/registry.py +514 -0
- mostlyright/data_harness/readers/samples/README.md +110 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.0.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/cities.csv.gz +0 -0
- mostlyright/data_harness/readers/samples/archive.gzip/1.1.0/cities_one_stream/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.0.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/cities.tar +0 -0
- mostlyright/data_harness/readers/samples/archive.tar/1.1.0/cities_beside_a_directory_entry/expected.json +24 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/cities.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.0.0/cities_beside_a_second_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.1.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/dwd-station.zip +0 -0
- mostlyright/data_harness/readers/samples/archive.zip/1.2.0/dwd_semicolon_station_member/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/cities.csv +3 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/an_ordinary_comma_separated_table/expected.json +23 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/cities.tsv +5 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.0.0/quoted_fields_holding_the_delimiter/expected.json +25 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/expected.json +30 -0
- mostlyright/data_harness/readers/samples/delimited_text/1.1.0/an_hourly_observation_table_served_as_plain_text/observations.csv +5 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/expected.json +44 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.0.0/nested_hourly_observations/stations.json +1 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/expected.json +48 -0
- mostlyright/data_harness/readers/samples/json.tabular/1.1.0/an_observation_stream_served_as_plain_text/observations.ndjson +4 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/cities.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/an_ordinary_table_beside_a_second_sheet/expected.json +24 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.0.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/expected.json +27 -0
- mostlyright/data_harness/readers/samples/spreadsheet.xlsx/1.1.0/shares_the_workbook_had_already_computed/shares.xlsx +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/README.md +20 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/expected.json +55 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/gfs_2m_temperature/gfs-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/1.0.0/hrrr_categorical_rain/hrrr-categorical-rain.grib2 +0 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/expected.json +54 -0
- mostlyright/data_harness/readers/samples/weather.grib2/2.0.0/hrrr_2m_temperature/hrrr-2m-temperature.grib2 +0 -0
- mostlyright/data_harness/readers/samples.py +582 -0
- mostlyright/data_harness/readers/spreadsheet.py +803 -0
- mostlyright/data_harness/readers/tabular.py +510 -0
- mostlyright/data_harness/recipe.py +5321 -0
- mostlyright/data_harness/repair/__init__.py +78 -0
- mostlyright/data_harness/repair/adapters.py +274 -0
- mostlyright/data_harness/repair/contracts.py +872 -0
- mostlyright/data_harness/repair/coordinator.py +1099 -0
- mostlyright/data_harness/repair/errors.py +16 -0
- mostlyright/data_harness/review.py +2533 -0
- mostlyright/data_harness/rowset.py +283 -0
- mostlyright/data_harness/serving.py +1975 -0
- mostlyright/data_harness/serving_edge.py +590 -0
- mostlyright/data_harness/serving_http.py +1031 -0
- mostlyright/data_harness/session_probes.py +759 -0
- mostlyright/data_harness/signing.py +101 -0
- mostlyright/data_harness/source_discovery.py +898 -0
- mostlyright/data_harness/sources/__init__.py +209 -0
- mostlyright/data_harness/sources/_adapter_steps.py +213 -0
- mostlyright/data_harness/sources/adapters.py +1214 -0
- mostlyright/data_harness/sources/cadence.py +1428 -0
- mostlyright/data_harness/sources/cadence_emission.py +453 -0
- mostlyright/data_harness/sources/cadence_history.py +546 -0
- mostlyright/data_harness/sources/catalog/__init__.py +17 -0
- mostlyright/data_harness/sources/catalog/admission.py +477 -0
- mostlyright/data_harness/sources/catalog/authoring.py +1701 -0
- mostlyright/data_harness/sources/catalog/authoring_policy.py +701 -0
- mostlyright/data_harness/sources/catalog/authoring_shards.py +1217 -0
- mostlyright/data_harness/sources/catalog/bounded_io.py +231 -0
- mostlyright/data_harness/sources/catalog/channel.py +523 -0
- mostlyright/data_harness/sources/catalog/channel_client.py +296 -0
- mostlyright/data_harness/sources/catalog/contracts.py +825 -0
- mostlyright/data_harness/sources/catalog/coverage.py +137 -0
- mostlyright/data_harness/sources/catalog/delta.py +1340 -0
- mostlyright/data_harness/sources/catalog/embedding.py +532 -0
- mostlyright/data_harness/sources/catalog/entry_v2.py +1182 -0
- mostlyright/data_harness/sources/catalog/fill.py +3889 -0
- mostlyright/data_harness/sources/catalog/fill_partitions.py +459 -0
- mostlyright/data_harness/sources/catalog/fill_staging.py +1105 -0
- mostlyright/data_harness/sources/catalog/gating.py +374 -0
- mostlyright/data_harness/sources/catalog/generation_receipt.py +1607 -0
- mostlyright/data_harness/sources/catalog/harvest/__init__.py +7 -0
- mostlyright/data_harness/sources/catalog/harvest/ckan.py +384 -0
- mostlyright/data_harness/sources/catalog/harvest/datagov_v4.py +798 -0
- mostlyright/data_harness/sources/catalog/harvest/protocol.py +964 -0
- mostlyright/data_harness/sources/catalog/harvest/sdmx.py +445 -0
- mostlyright/data_harness/sources/catalog/harvest/stac.py +384 -0
- mostlyright/data_harness/sources/catalog/health.py +447 -0
- mostlyright/data_harness/sources/catalog/hosted_catalog.py +105 -0
- mostlyright/data_harness/sources/catalog/identity_history.py +1549 -0
- mostlyright/data_harness/sources/catalog/neural.py +1618 -0
- mostlyright/data_harness/sources/catalog/packed_catalog.py +2345 -0
- mostlyright/data_harness/sources/catalog/packed_retrieval.py +1517 -0
- mostlyright/data_harness/sources/catalog/packed_writer.py +2802 -0
- mostlyright/data_harness/sources/catalog/query_trace.py +1037 -0
- mostlyright/data_harness/sources/catalog/recommend.py +171 -0
- mostlyright/data_harness/sources/catalog/retrieval.py +230 -0
- mostlyright/data_harness/sources/catalog/retrieval_manifest.py +995 -0
- mostlyright/data_harness/sources/catalog/rights_decisions.py +254 -0
- mostlyright/data_harness/sources/catalog/sealed.py +560 -0
- mostlyright/data_harness/sources/catalog/search.py +230 -0
- mostlyright/data_harness/sources/catalog/streaming_delta.py +1097 -0
- mostlyright/data_harness/sources/catalog/update.py +891 -0
- mostlyright/data_harness/sources/collections.py +815 -0
- mostlyright/data_harness/sources/contracts.py +2223 -0
- mostlyright/data_harness/sources/deletion.py +761 -0
- mostlyright/data_harness/sources/fitness.py +162 -0
- mostlyright/data_harness/sources/governance.py +163 -0
- mostlyright/data_harness/sources/hosted.py +173 -0
- mostlyright/data_harness/sources/integration.py +218 -0
- mostlyright/data_harness/sources/range_reader.py +418 -0
- mostlyright/data_harness/sources/registry.py +514 -0
- mostlyright/data_harness/sources/rights_rule.py +59 -0
- mostlyright/data_harness/sources/source_cadence_vectors.v1.json +1 -0
- mostlyright/data_harness/sources/sports.py +521 -0
- mostlyright/data_harness/sources/stream.py +524 -0
- mostlyright/data_harness/sources/stream_connector.py +418 -0
- mostlyright/data_harness/sources/stream_recorder.py +1404 -0
- mostlyright/data_harness/studio_boundary.py +2019 -0
- mostlyright/data_harness/thin/__init__.py +37 -0
- mostlyright/data_harness/thin/acquire.py +1137 -0
- mostlyright/data_harness/thin/acquire_cancel.py +579 -0
- mostlyright/data_harness/thin/approvals.py +617 -0
- mostlyright/data_harness/thin/commands.py +406 -0
- mostlyright/data_harness/thin/download.py +194 -0
- mostlyright/data_harness/thin/narrative.py +589 -0
- mostlyright/data_harness/thin/parity.py +1070 -0
- mostlyright/data_harness/thin/propose.py +2759 -0
- mostlyright/data_harness/thin/research.py +1663 -0
- mostlyright/data_harness/thin/router.py +924 -0
- mostlyright/data_harness/thin/runs.py +519 -0
- mostlyright/data_harness/thin/session.py +281 -0
- mostlyright/data_harness/thin/stream.py +501 -0
- mostlyright/data_harness/thin/transport.py +187 -0
- mostlyright/data_harness/thin/vocabulary.py +368 -0
- mostlyright/data_harness/thin/workers.py +164 -0
- mostlyright/data_harness/ucum/TABLE-PIN.json +40 -0
- mostlyright/data_harness/ucum/ucum-subset.v1.json +632 -0
- mostlyright/data_harness/unit_flow.py +927 -0
- mostlyright/data_harness/units.py +572 -0
- mostlyright/data_harness/ux/__init__.py +9 -0
- mostlyright/data_harness/ux/approve.py +485 -0
- mostlyright/data_harness/ux/author_yaml.py +597 -0
- mostlyright/data_harness/ux/cloud_auth.py +447 -0
- mostlyright/data_harness/ux/commands/__init__.py +260 -0
- mostlyright/data_harness/ux/commands/approve.py +136 -0
- mostlyright/data_harness/ux/commands/auth.py +744 -0
- mostlyright/data_harness/ux/commands/author.py +79 -0
- mostlyright/data_harness/ux/commands/catalog_author.py +403 -0
- mostlyright/data_harness/ux/commands/catalog_fill.py +523 -0
- mostlyright/data_harness/ux/commands/catalog_harvest.py +545 -0
- mostlyright/data_harness/ux/commands/catalog_publish.py +1838 -0
- mostlyright/data_harness/ux/commands/catalog_search.py +71 -0
- mostlyright/data_harness/ux/commands/catalog_update.py +437 -0
- mostlyright/data_harness/ux/commands/deploy.py +134 -0
- mostlyright/data_harness/ux/commands/deploy_dataset.py +98 -0
- mostlyright/data_harness/ux/commands/deploy_plan.py +105 -0
- mostlyright/data_harness/ux/commands/deploy_status.py +104 -0
- mostlyright/data_harness/ux/commands/diff.py +74 -0
- mostlyright/data_harness/ux/commands/index.py +84 -0
- mostlyright/data_harness/ux/commands/inventory.py +47 -0
- mostlyright/data_harness/ux/commands/list_builds.py +143 -0
- mostlyright/data_harness/ux/commands/login.py +63 -0
- mostlyright/data_harness/ux/commands/peek.py +236 -0
- mostlyright/data_harness/ux/commands/plan_check.py +90 -0
- mostlyright/data_harness/ux/commands/preflight.py +97 -0
- mostlyright/data_harness/ux/commands/record.py +107 -0
- mostlyright/data_harness/ux/commands/review_setup.py +47 -0
- mostlyright/data_harness/ux/commands/search.py +440 -0
- mostlyright/data_harness/ux/commands/show.py +61 -0
- mostlyright/data_harness/ux/commands/whoami.py +37 -0
- mostlyright/data_harness/ux/credential_native.py +551 -0
- mostlyright/data_harness/ux/credential_store.py +1055 -0
- mostlyright/data_harness/ux/credentials.py +631 -0
- mostlyright/data_harness/ux/diffing.py +444 -0
- mostlyright/data_harness/ux/headline.py +671 -0
- mostlyright/data_harness/ux/hosted_acquisition.py +974 -0
- mostlyright/data_harness/ux/hosted_run_status.py +619 -0
- mostlyright/data_harness/ux/inventory.py +427 -0
- mostlyright/data_harness/ux/local_review.py +375 -0
- mostlyright/data_harness/ux/login.py +691 -0
- mostlyright/data_harness/ux/path_kind.py +147 -0
- mostlyright/data_harness/ux/peek.py +1000 -0
- mostlyright/data_harness/ux/plain_file.py +178 -0
- mostlyright/data_harness/ux/plan_check.py +311 -0
- mostlyright/data_harness/ux/preflight.py +918 -0
- mostlyright/data_harness/ux/readers.py +1124 -0
- mostlyright/data_harness/ux/remediation.py +2195 -0
- mostlyright/data_harness/ux/render.py +657 -0
- mostlyright/data_harness/ux/workload.py +1077 -0
- mostlyright/data_harness/viewer.py +3713 -0
- mostlyright/data_harness/visual_run/__init__.py +83 -0
- mostlyright/data_harness/visual_run/authoring.py +235 -0
- mostlyright/data_harness/visual_run/contracts.py +673 -0
- mostlyright/data_harness/visual_run/materialize.py +486 -0
- mostlyright/data_harness/visual_run/observations.py +874 -0
- mostlyright/data_harness/visual_run/query.py +259 -0
- mostlyright/data_harness/visual_run/reducer.py +280 -0
- mostlyright/data_harness/visual_run/sdk.py +892 -0
- mostlyright/data_harness/visual_run/store.py +584 -0
- mostlyright/data_harness/visual_run/transport.py +239 -0
- mostlyright/data_harness/watch.py +2999 -0
- mostlyright_data-0.9.0.dist-info/METADATA +607 -0
- mostlyright_data-0.9.0.dist-info/RECORD +314 -0
- mostlyright_data-0.9.0.dist-info/WHEEL +4 -0
- mostlyright_data-0.9.0.dist-info/entry_points.txt +12 -0
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
"""Workload-identity bootstrap for hosted producer and verifier workers.
|
|
2
|
+
|
|
3
|
+
The launcher is the only process that sees the ambient workload identity response. It exchanges
|
|
4
|
+
that identity for one attempt-scoped Studio bootstrap envelope, validates the complete envelope,
|
|
5
|
+
places each worker input in a separate anonymous descriptor, sanitizes descriptor inheritance and
|
|
6
|
+
the environment, and then replaces itself with the fixed worker entrypoint.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import hashlib
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import ssl
|
|
16
|
+
import stat
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, NoReturn, Protocol
|
|
24
|
+
from urllib.error import HTTPError
|
|
25
|
+
from urllib.parse import urlencode, urlsplit
|
|
26
|
+
from urllib.request import (
|
|
27
|
+
HTTPRedirectHandler,
|
|
28
|
+
HTTPSHandler,
|
|
29
|
+
ProxyHandler,
|
|
30
|
+
Request,
|
|
31
|
+
build_opener,
|
|
32
|
+
)
|
|
33
|
+
from uuid import UUID
|
|
34
|
+
|
|
35
|
+
from mostlyright.data_harness import pipeline
|
|
36
|
+
from mostlyright.data_harness.canonical import canonical_json_bytes
|
|
37
|
+
from mostlyright.data_harness.governors import (
|
|
38
|
+
CapExceeded,
|
|
39
|
+
CapLedger,
|
|
40
|
+
CapStoreError,
|
|
41
|
+
FileCapStore,
|
|
42
|
+
Reservation,
|
|
43
|
+
)
|
|
44
|
+
from mostlyright.data_harness.hosted_worker import (
|
|
45
|
+
MAX_CAPABILITY_BYTES,
|
|
46
|
+
MAX_DESCRIPTOR_BYTES,
|
|
47
|
+
MAX_SIGNING_KEY_BYTES,
|
|
48
|
+
HostedWorkerError,
|
|
49
|
+
_strict_json,
|
|
50
|
+
parse_capability,
|
|
51
|
+
parse_producer_job,
|
|
52
|
+
parse_signing_key,
|
|
53
|
+
parse_verifier_job,
|
|
54
|
+
)
|
|
55
|
+
from mostlyright.data_harness.local_contracts import GraphTablePlan
|
|
56
|
+
|
|
57
|
+
# The workers have accepted V3 jobs since the Dataset/Table cutover. Bootstrap must speak the
|
|
58
|
+
# matching envelope too: accepting a V2 response here would otherwise let a legacy coordinator
|
|
59
|
+
# re-introduce the child-only identity that the job parser correctly rejects.
|
|
60
|
+
BOOTSTRAP_REQUEST_SCHEMA = "mostlyright-worker-bootstrap-request.v3"
|
|
61
|
+
BOOTSTRAP_RESPONSE_SCHEMA = "mostlyright-worker-bootstrap-response.v3"
|
|
62
|
+
BOOTSTRAP_EXCHANGE_PATH = "/_internal/worker-bootstrap:exchange"
|
|
63
|
+
METADATA_IDENTITY_URL = (
|
|
64
|
+
"http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity"
|
|
65
|
+
)
|
|
66
|
+
MAX_IDENTITY_BYTES = 16 * 1024
|
|
67
|
+
MAX_BOOTSTRAP_RESPONSE_BYTES = 768 * 1024
|
|
68
|
+
BOOTSTRAP_TIMEOUT_SECONDS = 10.0
|
|
69
|
+
WORKER_BIN_DIRECTORY = Path("/opt/worker/bin")
|
|
70
|
+
_JWT = re.compile(r"^[A-Za-z0-9_-]{1,8192}\.[A-Za-z0-9_-]{1,8192}\.[A-Za-z0-9_-]{1,8192}$")
|
|
71
|
+
_DISPATCH_QUERY = re.compile(
|
|
72
|
+
r"^dispatch_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$"
|
|
73
|
+
)
|
|
74
|
+
_POLICY_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
75
|
+
_IMAGE_DIGEST = re.compile(r"^[a-z0-9][a-z0-9._/-]{0,254}@sha256:[0-9a-f]{64}$")
|
|
76
|
+
_POLICY_SELECTION_ENV = "MOSTLYRIGHT_WORKER_POLICY_SELECTION_JSON"
|
|
77
|
+
_POLICY_BINDING_SCHEMA = "mostlyright-worker-policy-binding.v2"
|
|
78
|
+
|
|
79
|
+
#: Where the durable cap counters live. See :func:`admission_ledger` for what setting it and
|
|
80
|
+
#: leaving it unset each actually enforce.
|
|
81
|
+
CAP_STORE_ENV = "MOSTLYRIGHT_CAP_STORE"
|
|
82
|
+
|
|
83
|
+
#: What this deployment declares one run costs, in cost units (one cent each). See
|
|
84
|
+
#: :func:`declared_run_cost_units`: it is the operator's worst-case budget for a run, not a
|
|
85
|
+
#: measurement, and leaving it unset means the monthly spend ceiling cannot trip.
|
|
86
|
+
RUN_COST_UNITS_ENV = "MOSTLYRIGHT_RUN_COST_UNITS"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class BootstrapTransport(Protocol):
|
|
90
|
+
"""Minimal redirect-free, proxy-free bootstrap transport."""
|
|
91
|
+
|
|
92
|
+
def request(
|
|
93
|
+
self,
|
|
94
|
+
method: str,
|
|
95
|
+
url: str,
|
|
96
|
+
headers: Mapping[str, str],
|
|
97
|
+
body: bytes | None,
|
|
98
|
+
maximum: int,
|
|
99
|
+
) -> tuple[int, bytes]: ...
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class _NoRedirect(HTTPRedirectHandler):
|
|
103
|
+
def redirect_request(
|
|
104
|
+
self,
|
|
105
|
+
request: Request,
|
|
106
|
+
file_pointer: Any,
|
|
107
|
+
code: int,
|
|
108
|
+
message: str,
|
|
109
|
+
headers: Any,
|
|
110
|
+
new_url: str,
|
|
111
|
+
) -> None:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class UrlLibBootstrapTransport:
|
|
116
|
+
"""Small stdlib transport with ambient proxies and redirects disabled."""
|
|
117
|
+
|
|
118
|
+
def __init__(self) -> None:
|
|
119
|
+
self._opener = build_opener(
|
|
120
|
+
ProxyHandler({}),
|
|
121
|
+
HTTPSHandler(context=ssl.create_default_context()),
|
|
122
|
+
_NoRedirect(),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def request(
|
|
126
|
+
self,
|
|
127
|
+
method: str,
|
|
128
|
+
url: str,
|
|
129
|
+
headers: Mapping[str, str],
|
|
130
|
+
body: bytes | None,
|
|
131
|
+
maximum: int,
|
|
132
|
+
) -> tuple[int, bytes]:
|
|
133
|
+
request = Request(url, data=body, headers=dict(headers), method=method)
|
|
134
|
+
try:
|
|
135
|
+
response = self._opener.open(request, timeout=BOOTSTRAP_TIMEOUT_SECONDS)
|
|
136
|
+
except HTTPError as error:
|
|
137
|
+
response = error
|
|
138
|
+
with response:
|
|
139
|
+
status = int(response.status)
|
|
140
|
+
content_length = response.headers.get("Content-Length")
|
|
141
|
+
if content_length is not None:
|
|
142
|
+
try:
|
|
143
|
+
declared = int(content_length)
|
|
144
|
+
except ValueError as error:
|
|
145
|
+
raise HostedWorkerError(
|
|
146
|
+
"BOOTSTRAP_TRANSPORT_INVALID",
|
|
147
|
+
"bootstrap response has an invalid content length",
|
|
148
|
+
) from error
|
|
149
|
+
if declared < 0 or declared > maximum:
|
|
150
|
+
raise HostedWorkerError(
|
|
151
|
+
"BOOTSTRAP_TOO_LARGE",
|
|
152
|
+
"bootstrap response exceeds its byte limit",
|
|
153
|
+
)
|
|
154
|
+
raw = response.read(maximum + 1)
|
|
155
|
+
if len(raw) > maximum:
|
|
156
|
+
raise HostedWorkerError(
|
|
157
|
+
"BOOTSTRAP_TOO_LARGE",
|
|
158
|
+
"bootstrap response exceeds its byte limit",
|
|
159
|
+
)
|
|
160
|
+
return status, raw
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class BootstrapConfig:
|
|
165
|
+
exchange_url: str
|
|
166
|
+
audience: str
|
|
167
|
+
role: str
|
|
168
|
+
worker_policy_binding: WorkerPolicyBinding | None = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _policy_digest(value: Any, label: str) -> str:
|
|
172
|
+
if (
|
|
173
|
+
not isinstance(value, str)
|
|
174
|
+
or not _POLICY_DIGEST.fullmatch(value)
|
|
175
|
+
or value == "sha256:" + "0" * 64
|
|
176
|
+
):
|
|
177
|
+
raise HostedWorkerError(
|
|
178
|
+
"WORKER_POLICY_BINDING_INVALID",
|
|
179
|
+
f"{label} is not a SHA-256 digest",
|
|
180
|
+
)
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _image_digest(value: Any, label: str) -> str:
|
|
185
|
+
if (
|
|
186
|
+
not isinstance(value, str)
|
|
187
|
+
or not _IMAGE_DIGEST.fullmatch(value)
|
|
188
|
+
or value.endswith("@sha256:" + "0" * 64)
|
|
189
|
+
):
|
|
190
|
+
raise HostedWorkerError(
|
|
191
|
+
"WORKER_POLICY_BINDING_INVALID",
|
|
192
|
+
f"{label} is not immutable",
|
|
193
|
+
)
|
|
194
|
+
return value
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def parse_worker_policy_binding(value: Any) -> WorkerPolicyBinding:
|
|
198
|
+
"""Parse the exact deploy-owned V2 policy-set binding and recompute both digests."""
|
|
199
|
+
|
|
200
|
+
expected = {
|
|
201
|
+
"schema_version",
|
|
202
|
+
"producer_image_digest",
|
|
203
|
+
"verifier_image_digest",
|
|
204
|
+
"validation_policy_digests",
|
|
205
|
+
"policy_set_digest",
|
|
206
|
+
"release_revision_coordinate",
|
|
207
|
+
"selection_id",
|
|
208
|
+
}
|
|
209
|
+
if not isinstance(value, dict) or set(value) != expected:
|
|
210
|
+
raise HostedWorkerError(
|
|
211
|
+
"WORKER_POLICY_BINDING_INVALID",
|
|
212
|
+
"worker policy binding fields are not exact",
|
|
213
|
+
)
|
|
214
|
+
if value["schema_version"] != _POLICY_BINDING_SCHEMA:
|
|
215
|
+
raise HostedWorkerError(
|
|
216
|
+
"WORKER_POLICY_BINDING_INVALID",
|
|
217
|
+
"worker policy binding schema is invalid",
|
|
218
|
+
)
|
|
219
|
+
digests = value["validation_policy_digests"]
|
|
220
|
+
if (
|
|
221
|
+
not isinstance(digests, dict)
|
|
222
|
+
or set(digests) != {"table", "graph"}
|
|
223
|
+
or any(not isinstance(mode, str) for mode in digests)
|
|
224
|
+
):
|
|
225
|
+
raise HostedWorkerError(
|
|
226
|
+
"WORKER_POLICY_BINDING_INVALID",
|
|
227
|
+
"worker policy digest set must contain exactly table and graph",
|
|
228
|
+
)
|
|
229
|
+
policies = {
|
|
230
|
+
mode: _policy_digest(digests[mode], f"validation_policy_digests.{mode}")
|
|
231
|
+
for mode in ("table", "graph")
|
|
232
|
+
}
|
|
233
|
+
policy_set_digest = _policy_digest(value["policy_set_digest"], "policy_set_digest")
|
|
234
|
+
if "sha256:" + hashlib.sha256(canonical_json_bytes(policies)).hexdigest() != policy_set_digest:
|
|
235
|
+
raise HostedWorkerError(
|
|
236
|
+
"WORKER_POLICY_BINDING_INVALID",
|
|
237
|
+
"policy set digest does not bind the exact table and graph digests",
|
|
238
|
+
)
|
|
239
|
+
selection_id = value["selection_id"]
|
|
240
|
+
if (
|
|
241
|
+
not isinstance(selection_id, str)
|
|
242
|
+
or not re.fullmatch(r"[0-9a-f]{64}", selection_id)
|
|
243
|
+
or selection_id == "0" * 64
|
|
244
|
+
):
|
|
245
|
+
raise HostedWorkerError(
|
|
246
|
+
"WORKER_POLICY_BINDING_INVALID",
|
|
247
|
+
"selection ID is invalid",
|
|
248
|
+
)
|
|
249
|
+
binding = WorkerPolicyBinding(
|
|
250
|
+
producer_image_digest=_image_digest(
|
|
251
|
+
value["producer_image_digest"], "producer_image_digest"
|
|
252
|
+
),
|
|
253
|
+
verifier_image_digest=_image_digest(
|
|
254
|
+
value["verifier_image_digest"], "verifier_image_digest"
|
|
255
|
+
),
|
|
256
|
+
validation_policy_digests=policies,
|
|
257
|
+
policy_set_digest=policy_set_digest,
|
|
258
|
+
release_revision_coordinate=_policy_digest(
|
|
259
|
+
value["release_revision_coordinate"], "release_revision_coordinate"
|
|
260
|
+
),
|
|
261
|
+
selection_id=selection_id,
|
|
262
|
+
)
|
|
263
|
+
unsigned = binding.to_dict()
|
|
264
|
+
unsigned.pop("selection_id")
|
|
265
|
+
if hashlib.sha256(canonical_json_bytes(unsigned)).hexdigest() != selection_id:
|
|
266
|
+
raise HostedWorkerError(
|
|
267
|
+
"WORKER_POLICY_BINDING_INVALID",
|
|
268
|
+
"selection ID does not bind the exact worker policy selection",
|
|
269
|
+
)
|
|
270
|
+
return binding
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _local_validation_policy_digests() -> dict[str, str]:
|
|
274
|
+
return {
|
|
275
|
+
"table": "sha256:" + pipeline.validation_policy_digest(graph=False),
|
|
276
|
+
"graph": "sha256:" + pipeline.validation_policy_digest(graph=True),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _require_local_policy_binding(binding: WorkerPolicyBinding) -> None:
|
|
281
|
+
local = _local_validation_policy_digests()
|
|
282
|
+
if binding.validation_policy_digests != local:
|
|
283
|
+
deployed = canonical_json_bytes(binding.validation_policy_digests).decode("utf-8")
|
|
284
|
+
computed = canonical_json_bytes(local).decode("utf-8")
|
|
285
|
+
raise HostedWorkerError(
|
|
286
|
+
"WORKER_POLICY_RUNTIME_MISMATCH",
|
|
287
|
+
"deployed validation policy set "
|
|
288
|
+
f"{deployed} differs from this worker runtime {computed}; "
|
|
289
|
+
"publish and deploy worker images whose receipt carries the runtime policy set",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _binding_from_environment(environment: Mapping[str, str]) -> WorkerPolicyBinding:
|
|
294
|
+
raw = environment.get(_POLICY_SELECTION_ENV)
|
|
295
|
+
if not isinstance(raw, str):
|
|
296
|
+
raise HostedWorkerError(
|
|
297
|
+
"WORKER_POLICY_BINDING_MISSING",
|
|
298
|
+
"worker policy selection is not configured in this worker template",
|
|
299
|
+
)
|
|
300
|
+
return parse_worker_policy_binding(_strict_json(raw.encode("utf-8"), "worker policy selection"))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass(frozen=True)
|
|
304
|
+
class WorkerPolicyBinding:
|
|
305
|
+
producer_image_digest: str
|
|
306
|
+
verifier_image_digest: str
|
|
307
|
+
validation_policy_digests: dict[str, str]
|
|
308
|
+
policy_set_digest: str
|
|
309
|
+
release_revision_coordinate: str
|
|
310
|
+
selection_id: str
|
|
311
|
+
|
|
312
|
+
def to_dict(self) -> dict[str, Any]:
|
|
313
|
+
return {
|
|
314
|
+
"schema_version": _POLICY_BINDING_SCHEMA,
|
|
315
|
+
"producer_image_digest": self.producer_image_digest,
|
|
316
|
+
"verifier_image_digest": self.verifier_image_digest,
|
|
317
|
+
"validation_policy_digests": dict(self.validation_policy_digests),
|
|
318
|
+
"policy_set_digest": self.policy_set_digest,
|
|
319
|
+
"release_revision_coordinate": self.release_revision_coordinate,
|
|
320
|
+
"selection_id": self.selection_id,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass
|
|
325
|
+
class _AnonymousPayload:
|
|
326
|
+
descriptor: int
|
|
327
|
+
handle: Any
|
|
328
|
+
|
|
329
|
+
def close(self) -> None:
|
|
330
|
+
self.handle.close()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _https_url(value: str, label: str) -> str:
|
|
334
|
+
if not isinstance(value, str) or not 1 <= len(value) <= 2048 or value.endswith("/"):
|
|
335
|
+
raise HostedWorkerError("BOOTSTRAP_CONFIG_INVALID", f"{label} is invalid")
|
|
336
|
+
parsed = urlsplit(value)
|
|
337
|
+
if (
|
|
338
|
+
parsed.scheme != "https"
|
|
339
|
+
or not parsed.hostname
|
|
340
|
+
or parsed.username is not None
|
|
341
|
+
or parsed.password is not None
|
|
342
|
+
or parsed.fragment
|
|
343
|
+
):
|
|
344
|
+
raise HostedWorkerError(
|
|
345
|
+
"BOOTSTRAP_CONFIG_INVALID",
|
|
346
|
+
f"{label} must be an absolute HTTPS URL without user information or fragment",
|
|
347
|
+
)
|
|
348
|
+
return value
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _v3_exchange_url(value: str) -> str:
|
|
352
|
+
_https_url(value, "exchange_url")
|
|
353
|
+
parsed = urlsplit(value)
|
|
354
|
+
matched = _DISPATCH_QUERY.fullmatch(parsed.query)
|
|
355
|
+
if parsed.path != BOOTSTRAP_EXCHANGE_PATH or matched is None:
|
|
356
|
+
raise HostedWorkerError(
|
|
357
|
+
"BOOTSTRAP_CONFIG_INVALID",
|
|
358
|
+
"exchange_url must use the exact bootstrap callback path "
|
|
359
|
+
f"{BOOTSTRAP_EXCHANGE_PATH} with one canonical UUIDv5 dispatch_id",
|
|
360
|
+
)
|
|
361
|
+
dispatch_id = matched.group(1)
|
|
362
|
+
try:
|
|
363
|
+
parsed_dispatch_id = UUID(dispatch_id)
|
|
364
|
+
except ValueError as error:
|
|
365
|
+
raise HostedWorkerError(
|
|
366
|
+
"BOOTSTRAP_CONFIG_INVALID",
|
|
367
|
+
"exchange_url dispatch_id must be a canonical UUIDv5",
|
|
368
|
+
) from error
|
|
369
|
+
if parsed_dispatch_id.version != 5 or str(parsed_dispatch_id) != dispatch_id:
|
|
370
|
+
raise HostedWorkerError(
|
|
371
|
+
"BOOTSTRAP_CONFIG_INVALID",
|
|
372
|
+
"exchange_url dispatch_id must be a canonical UUIDv5",
|
|
373
|
+
)
|
|
374
|
+
return value
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _metadata_identity(
|
|
378
|
+
transport: BootstrapTransport,
|
|
379
|
+
audience: str,
|
|
380
|
+
) -> bytearray:
|
|
381
|
+
query = urlencode({"audience": audience, "format": "full"})
|
|
382
|
+
status, raw = transport.request(
|
|
383
|
+
"GET",
|
|
384
|
+
f"{METADATA_IDENTITY_URL}?{query}",
|
|
385
|
+
{"Metadata-Flavor": "Google"},
|
|
386
|
+
None,
|
|
387
|
+
MAX_IDENTITY_BYTES,
|
|
388
|
+
)
|
|
389
|
+
if status != 200:
|
|
390
|
+
raise HostedWorkerError(
|
|
391
|
+
"WORKLOAD_IDENTITY_UNAVAILABLE",
|
|
392
|
+
f"metadata identity endpoint returned HTTP {status}",
|
|
393
|
+
)
|
|
394
|
+
try:
|
|
395
|
+
token = raw.decode("ascii")
|
|
396
|
+
except UnicodeDecodeError as error:
|
|
397
|
+
raise HostedWorkerError(
|
|
398
|
+
"WORKLOAD_IDENTITY_INVALID",
|
|
399
|
+
"metadata identity response is not ASCII",
|
|
400
|
+
) from error
|
|
401
|
+
if not _JWT.fullmatch(token):
|
|
402
|
+
raise HostedWorkerError(
|
|
403
|
+
"WORKLOAD_IDENTITY_INVALID",
|
|
404
|
+
"metadata identity response is not a compact JWT",
|
|
405
|
+
)
|
|
406
|
+
return bytearray(raw)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _exchange(
|
|
410
|
+
config: BootstrapConfig,
|
|
411
|
+
transport: BootstrapTransport,
|
|
412
|
+
identity: bytearray,
|
|
413
|
+
) -> dict[str, Any]:
|
|
414
|
+
body = canonical_json_bytes(
|
|
415
|
+
{
|
|
416
|
+
"schema_version": BOOTSTRAP_REQUEST_SCHEMA,
|
|
417
|
+
"role": config.role,
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
status, raw = transport.request(
|
|
421
|
+
"POST",
|
|
422
|
+
config.exchange_url,
|
|
423
|
+
{
|
|
424
|
+
"Accept": "application/json",
|
|
425
|
+
"Authorization": "Bearer " + identity.decode("ascii"),
|
|
426
|
+
"Content-Type": "application/json",
|
|
427
|
+
},
|
|
428
|
+
body,
|
|
429
|
+
MAX_BOOTSTRAP_RESPONSE_BYTES,
|
|
430
|
+
)
|
|
431
|
+
if status != 200:
|
|
432
|
+
raise HostedWorkerError(
|
|
433
|
+
"BOOTSTRAP_EXCHANGE_FAILED",
|
|
434
|
+
f"Studio bootstrap exchange returned HTTP {status}",
|
|
435
|
+
)
|
|
436
|
+
value = _strict_json(raw, "bootstrap response")
|
|
437
|
+
if not isinstance(value, dict):
|
|
438
|
+
raise HostedWorkerError(
|
|
439
|
+
"BOOTSTRAP_RESPONSE_INVALID",
|
|
440
|
+
"bootstrap response must be an object",
|
|
441
|
+
)
|
|
442
|
+
return value
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _require_worker_policy_preflight(
|
|
446
|
+
*,
|
|
447
|
+
job_wire: Any,
|
|
448
|
+
job: Any,
|
|
449
|
+
response_preflight: Any,
|
|
450
|
+
binding: WorkerPolicyBinding,
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Bind the deployed selection, Studio response, and parsed recipe mode before execution."""
|
|
453
|
+
|
|
454
|
+
if not isinstance(job_wire, dict):
|
|
455
|
+
raise HostedWorkerError(
|
|
456
|
+
"WORKER_POLICY_BINDING_MISMATCH",
|
|
457
|
+
"bootstrap job is not an object",
|
|
458
|
+
)
|
|
459
|
+
job_preflight = job_wire.get("worker_policy_preflight")
|
|
460
|
+
if not isinstance(job_preflight, dict) or response_preflight != job_preflight:
|
|
461
|
+
raise HostedWorkerError(
|
|
462
|
+
"WORKER_POLICY_BINDING_MISMATCH",
|
|
463
|
+
"Studio bootstrap response and job do not carry one exact policy preflight",
|
|
464
|
+
)
|
|
465
|
+
expected = {
|
|
466
|
+
"schema_version",
|
|
467
|
+
"workspace_id",
|
|
468
|
+
"dataset_id",
|
|
469
|
+
"table_plan_id",
|
|
470
|
+
"table_recipe_id",
|
|
471
|
+
"recipe_version",
|
|
472
|
+
"recipe_digest",
|
|
473
|
+
"builder_image_digest",
|
|
474
|
+
"verifier_image_digest",
|
|
475
|
+
"policy_mode",
|
|
476
|
+
"validation_policy_digest",
|
|
477
|
+
"validation_policy_digests",
|
|
478
|
+
"policy_set_digest",
|
|
479
|
+
"selection_fence",
|
|
480
|
+
"selection_id",
|
|
481
|
+
}
|
|
482
|
+
fields = set(job_preflight)
|
|
483
|
+
if (
|
|
484
|
+
fields not in (expected, expected | {"table_id"})
|
|
485
|
+
or job_preflight.get("schema_version") != "3.0.0"
|
|
486
|
+
):
|
|
487
|
+
raise HostedWorkerError(
|
|
488
|
+
"WORKER_POLICY_BINDING_MISMATCH",
|
|
489
|
+
"worker policy preflight fields are not exact",
|
|
490
|
+
)
|
|
491
|
+
mode = "graph" if isinstance(job.documents.recipe.transform_plan, GraphTablePlan) else "table"
|
|
492
|
+
expected_scope = {
|
|
493
|
+
"workspace_id": str(job.workspace_id),
|
|
494
|
+
"dataset_id": str(job.dataset_id),
|
|
495
|
+
"table_plan_id": str(job.table_plan_id),
|
|
496
|
+
"table_recipe_id": job.table_recipe_id,
|
|
497
|
+
"recipe_version": job.recipe_version,
|
|
498
|
+
"recipe_digest": job.documents.recipe_digest,
|
|
499
|
+
}
|
|
500
|
+
if any(job_preflight.get(name) != value for name, value in expected_scope.items()) or (
|
|
501
|
+
"table_id" in job_preflight and job_preflight["table_id"] != str(job.table_id)
|
|
502
|
+
):
|
|
503
|
+
raise HostedWorkerError(
|
|
504
|
+
"WORKER_POLICY_BINDING_MISMATCH",
|
|
505
|
+
"worker policy preflight scope differs from the bootstrap job",
|
|
506
|
+
)
|
|
507
|
+
static = {
|
|
508
|
+
"builder_image_digest": binding.producer_image_digest,
|
|
509
|
+
"verifier_image_digest": binding.verifier_image_digest,
|
|
510
|
+
"validation_policy_digests": binding.validation_policy_digests,
|
|
511
|
+
"policy_set_digest": binding.policy_set_digest,
|
|
512
|
+
"selection_fence": binding.release_revision_coordinate,
|
|
513
|
+
"selection_id": binding.selection_id,
|
|
514
|
+
"policy_mode": mode,
|
|
515
|
+
"validation_policy_digest": binding.validation_policy_digests[mode],
|
|
516
|
+
}
|
|
517
|
+
mismatched = [name for name, value in static.items() if job_preflight.get(name) != value]
|
|
518
|
+
if mismatched:
|
|
519
|
+
raise HostedWorkerError(
|
|
520
|
+
"WORKER_POLICY_BINDING_MISMATCH",
|
|
521
|
+
"worker policy preflight differs from the deployed selection: " + ",".join(mismatched),
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _validate_bootstrap_response(
|
|
526
|
+
value: dict[str, Any],
|
|
527
|
+
role: str,
|
|
528
|
+
*,
|
|
529
|
+
worker_policy_binding: WorkerPolicyBinding | None = None,
|
|
530
|
+
) -> tuple[dict[str, bytes], Any]:
|
|
531
|
+
"""Return the worker payload bytes and the parsed job the validation already produced.
|
|
532
|
+
|
|
533
|
+
The parsed job is returned rather than discarded because the admission gate needs the tenant
|
|
534
|
+
coordinate out of it, and reparsing to get it would mean two parses that could disagree.
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
common = {"schema_version", "role", "job", "capability"}
|
|
538
|
+
expected = common if role == "producer" else common | {"signing_key"}
|
|
539
|
+
if worker_policy_binding is not None:
|
|
540
|
+
expected = expected | {"worker_policy_preflight"}
|
|
541
|
+
if set(value) != expected:
|
|
542
|
+
raise HostedWorkerError(
|
|
543
|
+
"BOOTSTRAP_RESPONSE_INVALID",
|
|
544
|
+
"bootstrap response fields are not exact for the requested role",
|
|
545
|
+
)
|
|
546
|
+
if value["schema_version"] != BOOTSTRAP_RESPONSE_SCHEMA or value["role"] != role:
|
|
547
|
+
raise HostedWorkerError(
|
|
548
|
+
"BOOTSTRAP_RESPONSE_INVALID",
|
|
549
|
+
"bootstrap response schema or role mismatches",
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
job_wire = value["job"]
|
|
553
|
+
if not isinstance(job_wire, dict):
|
|
554
|
+
raise HostedWorkerError(
|
|
555
|
+
"BOOTSTRAP_RESPONSE_INVALID",
|
|
556
|
+
"bootstrap job must be an object",
|
|
557
|
+
)
|
|
558
|
+
job = dict(job_wire)
|
|
559
|
+
if worker_policy_binding is not None:
|
|
560
|
+
job.pop("worker_policy_preflight", None)
|
|
561
|
+
capability = value["capability"]
|
|
562
|
+
if not isinstance(capability, str):
|
|
563
|
+
raise HostedWorkerError(
|
|
564
|
+
"BOOTSTRAP_RESPONSE_INVALID",
|
|
565
|
+
"bootstrap capability must be a string",
|
|
566
|
+
)
|
|
567
|
+
capability_raw = capability.encode("ascii", errors="strict")
|
|
568
|
+
if len(capability_raw) > MAX_CAPABILITY_BYTES:
|
|
569
|
+
raise HostedWorkerError("BOOTSTRAP_TOO_LARGE", "capability exceeds its byte limit")
|
|
570
|
+
parse_capability(capability_raw)
|
|
571
|
+
job_raw = canonical_json_bytes(job)
|
|
572
|
+
if len(job_raw) > MAX_DESCRIPTOR_BYTES:
|
|
573
|
+
raise HostedWorkerError("BOOTSTRAP_TOO_LARGE", "worker job exceeds its byte limit")
|
|
574
|
+
if role == "producer":
|
|
575
|
+
parsed_job = parse_producer_job(job)
|
|
576
|
+
if worker_policy_binding is not None:
|
|
577
|
+
_require_worker_policy_preflight(
|
|
578
|
+
job_wire=job_wire,
|
|
579
|
+
job=parsed_job,
|
|
580
|
+
response_preflight=value["worker_policy_preflight"],
|
|
581
|
+
binding=worker_policy_binding,
|
|
582
|
+
)
|
|
583
|
+
return {"job": job_raw, "capability": capability_raw}, parsed_job
|
|
584
|
+
|
|
585
|
+
parsed_job = parse_verifier_job(job)
|
|
586
|
+
if worker_policy_binding is not None:
|
|
587
|
+
_require_worker_policy_preflight(
|
|
588
|
+
job_wire=job_wire,
|
|
589
|
+
job=parsed_job,
|
|
590
|
+
response_preflight=value["worker_policy_preflight"],
|
|
591
|
+
binding=worker_policy_binding,
|
|
592
|
+
)
|
|
593
|
+
signing_key = value["signing_key"]
|
|
594
|
+
parse_signing_key(signing_key)
|
|
595
|
+
signing_raw = canonical_json_bytes(signing_key)
|
|
596
|
+
if len(signing_raw) > MAX_SIGNING_KEY_BYTES:
|
|
597
|
+
raise HostedWorkerError("BOOTSTRAP_TOO_LARGE", "signing key exceeds its byte limit")
|
|
598
|
+
return {
|
|
599
|
+
"job": job_raw,
|
|
600
|
+
"capability": capability_raw,
|
|
601
|
+
"signing_key": signing_raw,
|
|
602
|
+
}, parsed_job
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _anonymous_payload(raw: bytes, label: str) -> _AnonymousPayload:
|
|
606
|
+
if hasattr(os, "memfd_create"):
|
|
607
|
+
descriptor = os.memfd_create( # type: ignore[attr-defined]
|
|
608
|
+
f"mostlyright-{label}",
|
|
609
|
+
flags=getattr(os, "MFD_CLOEXEC", 0),
|
|
610
|
+
)
|
|
611
|
+
handle = os.fdopen(descriptor, "w+b", closefd=True)
|
|
612
|
+
else:
|
|
613
|
+
# macOS development fallback. TemporaryFile is opened mode 0600 and immediately unlinked;
|
|
614
|
+
# production Linux images use memfd_create and never materialize the payload on disk.
|
|
615
|
+
handle = tempfile.TemporaryFile(mode="w+b", prefix=f"mostlyright-{label}-")
|
|
616
|
+
descriptor = handle.fileno()
|
|
617
|
+
handle.write(raw)
|
|
618
|
+
handle.flush()
|
|
619
|
+
handle.seek(0)
|
|
620
|
+
os.set_inheritable(descriptor, True)
|
|
621
|
+
return _AnonymousPayload(descriptor=descriptor, handle=handle)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _validate_worker_executable(
|
|
625
|
+
executable: Path,
|
|
626
|
+
role: str,
|
|
627
|
+
*,
|
|
628
|
+
chain_root: Path = Path("/"),
|
|
629
|
+
trusted_uid: int = 0,
|
|
630
|
+
) -> str:
|
|
631
|
+
"""Validate one fixed worker executable without following mutable path indirection."""
|
|
632
|
+
|
|
633
|
+
name = f"mr-data-{role}"
|
|
634
|
+
if role not in {"producer", "verifier"} or executable.name != name:
|
|
635
|
+
raise HostedWorkerError(
|
|
636
|
+
"WORKER_EXECUTABLE_UNAVAILABLE",
|
|
637
|
+
f"fixed {name} entrypoint is invalid",
|
|
638
|
+
)
|
|
639
|
+
if (
|
|
640
|
+
not executable.is_absolute()
|
|
641
|
+
or not chain_root.is_absolute()
|
|
642
|
+
or any(part in {".", ".."} for part in executable.parts)
|
|
643
|
+
):
|
|
644
|
+
raise HostedWorkerError(
|
|
645
|
+
"WORKER_EXECUTABLE_UNAVAILABLE",
|
|
646
|
+
f"fixed {name} entrypoint path is invalid",
|
|
647
|
+
)
|
|
648
|
+
try:
|
|
649
|
+
relative = executable.relative_to(chain_root)
|
|
650
|
+
except ValueError as error:
|
|
651
|
+
raise HostedWorkerError(
|
|
652
|
+
"WORKER_EXECUTABLE_UNAVAILABLE",
|
|
653
|
+
f"fixed {name} entrypoint escapes its trusted path chain",
|
|
654
|
+
) from error
|
|
655
|
+
|
|
656
|
+
current = chain_root
|
|
657
|
+
components = (
|
|
658
|
+
current,
|
|
659
|
+
*(current / Path(*relative.parts[:index]) for index in range(1, len(relative.parts) + 1)),
|
|
660
|
+
)
|
|
661
|
+
for index, component in enumerate(components):
|
|
662
|
+
try:
|
|
663
|
+
metadata = component.lstat()
|
|
664
|
+
except OSError as error:
|
|
665
|
+
raise HostedWorkerError(
|
|
666
|
+
"WORKER_EXECUTABLE_UNAVAILABLE",
|
|
667
|
+
f"fixed {name} entrypoint is not installed",
|
|
668
|
+
) from error
|
|
669
|
+
is_last = index == len(components) - 1
|
|
670
|
+
if stat.S_ISLNK(metadata.st_mode):
|
|
671
|
+
raise HostedWorkerError(
|
|
672
|
+
"WORKER_EXECUTABLE_UNSAFE",
|
|
673
|
+
f"fixed {name} entrypoint path contains a symbolic link",
|
|
674
|
+
)
|
|
675
|
+
if metadata.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
|
|
676
|
+
raise HostedWorkerError(
|
|
677
|
+
"WORKER_EXECUTABLE_UNSAFE",
|
|
678
|
+
f"fixed {name} entrypoint path is group or world writable",
|
|
679
|
+
)
|
|
680
|
+
if metadata.st_uid != trusted_uid:
|
|
681
|
+
raise HostedWorkerError(
|
|
682
|
+
"WORKER_EXECUTABLE_UNSAFE",
|
|
683
|
+
f"fixed {name} entrypoint path is not owned by the trusted uid",
|
|
684
|
+
)
|
|
685
|
+
if (is_last and not stat.S_ISREG(metadata.st_mode)) or (
|
|
686
|
+
not is_last and not stat.S_ISDIR(metadata.st_mode)
|
|
687
|
+
):
|
|
688
|
+
raise HostedWorkerError(
|
|
689
|
+
"WORKER_EXECUTABLE_UNSAFE",
|
|
690
|
+
f"fixed {name} entrypoint path has an invalid file type",
|
|
691
|
+
)
|
|
692
|
+
if not os.access(executable, os.X_OK):
|
|
693
|
+
raise HostedWorkerError(
|
|
694
|
+
"WORKER_EXECUTABLE_UNAVAILABLE",
|
|
695
|
+
f"fixed {name} entrypoint is not executable",
|
|
696
|
+
)
|
|
697
|
+
return str(executable)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _worker_executable(role: str) -> str:
|
|
701
|
+
"""Resolve only the immutable container installation coordinate."""
|
|
702
|
+
|
|
703
|
+
return _validate_worker_executable(WORKER_BIN_DIRECTORY / f"mr-data-{role}", role)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _worker_argv(
|
|
707
|
+
executable: str, role: str, payloads: Mapping[str, _AnonymousPayload]
|
|
708
|
+
) -> list[str]:
|
|
709
|
+
arguments = [
|
|
710
|
+
executable,
|
|
711
|
+
"--job-fd",
|
|
712
|
+
str(payloads["job"].descriptor),
|
|
713
|
+
"--capability-fd",
|
|
714
|
+
str(payloads["capability"].descriptor),
|
|
715
|
+
]
|
|
716
|
+
if role == "verifier":
|
|
717
|
+
arguments.extend(
|
|
718
|
+
[
|
|
719
|
+
"--signing-key-fd",
|
|
720
|
+
str(payloads["signing_key"].descriptor),
|
|
721
|
+
]
|
|
722
|
+
)
|
|
723
|
+
return arguments
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _restrict_inherited_descriptors(allowed: set[int]) -> list[int]:
|
|
727
|
+
directory = next(
|
|
728
|
+
(Path(candidate) for candidate in ("/proc/self/fd", "/dev/fd") if Path(candidate).is_dir()),
|
|
729
|
+
None,
|
|
730
|
+
)
|
|
731
|
+
if directory is None:
|
|
732
|
+
raise HostedWorkerError(
|
|
733
|
+
"DESCRIPTOR_SANITIZATION_UNAVAILABLE",
|
|
734
|
+
"cannot enumerate descriptors before worker process replacement",
|
|
735
|
+
)
|
|
736
|
+
changed: list[int] = []
|
|
737
|
+
for entry in directory.iterdir():
|
|
738
|
+
try:
|
|
739
|
+
descriptor = int(entry.name)
|
|
740
|
+
except ValueError:
|
|
741
|
+
continue
|
|
742
|
+
if descriptor <= 2 or descriptor in allowed:
|
|
743
|
+
continue
|
|
744
|
+
try:
|
|
745
|
+
if os.get_inheritable(descriptor):
|
|
746
|
+
os.set_inheritable(descriptor, False)
|
|
747
|
+
changed.append(descriptor)
|
|
748
|
+
except OSError:
|
|
749
|
+
# The enumeration directory itself, or another concurrently closed descriptor, can
|
|
750
|
+
# disappear between listing and inspection. It cannot survive exec in that state.
|
|
751
|
+
continue
|
|
752
|
+
return changed
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _restore_inherited_descriptors(descriptors: Sequence[int]) -> None:
|
|
756
|
+
for descriptor in descriptors:
|
|
757
|
+
try:
|
|
758
|
+
os.set_inheritable(descriptor, True)
|
|
759
|
+
except OSError:
|
|
760
|
+
continue
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def admission_ledger() -> CapLedger:
|
|
764
|
+
"""Build the workspace admission ledger.
|
|
765
|
+
|
|
766
|
+
``MOSTLYRIGHT_CAP_STORE`` must name shared POSIX storage for monthly limits to persist across
|
|
767
|
+
job executions. The store requires advisory locking and atomic rename and must be initialized
|
|
768
|
+
before use. Without it, counters are process-local. Admission charges the run before execution.
|
|
769
|
+
|
|
770
|
+
Spend uses ``MOSTLYRIGHT_RUN_COST_UNITS``. The ledger uses ``CapPolicy`` defaults because this
|
|
771
|
+
entry point receives no per-workspace policy. It does not persist concurrency reservations.
|
|
772
|
+
"""
|
|
773
|
+
|
|
774
|
+
configured = os.environ.get(CAP_STORE_ENV)
|
|
775
|
+
if configured:
|
|
776
|
+
return CapLedger(store=FileCapStore(Path(configured)), subject="workspace")
|
|
777
|
+
return CapLedger(subject="workspace")
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _tenant_coordinate(workspace_id: Any) -> str:
|
|
781
|
+
"""Render a workspace id into the 16-lowercase-hex coordinate the cap ledger counts by.
|
|
782
|
+
|
|
783
|
+
The tenant coordinate on this path is ``workspace_id``, taken from the validated bootstrap job.
|
|
784
|
+
It is deliberately NOT an API key id: ``key_id`` in this repository names the attestation
|
|
785
|
+
signing key (``hosted_worker.SigningKey.key_id``), which is a different concept, and the
|
|
786
|
+
customer API key never reaches this process at all (see ``key_seam.CONSUMER_CALL_SITE``).
|
|
787
|
+
|
|
788
|
+
The ledger's coordinate shape is cloud's ``hashKeyId`` shape — the first 8 bytes of a SHA-256
|
|
789
|
+
digest, hex-encoded — and that shape check is what makes it impossible to file a raw credential
|
|
790
|
+
where an identifier belongs. The same derivation is applied to the workspace id here, so the
|
|
791
|
+
check still holds. The honest consequence: these counters are per workspace, so they do not
|
|
792
|
+
line up with cloud's ``usage_events.key_id`` rows, which are per API key. This ledger bounds a
|
|
793
|
+
workspace's runs; it is not the billing attribution record.
|
|
794
|
+
"""
|
|
795
|
+
|
|
796
|
+
return hashlib.sha256(str(workspace_id).encode("utf-8")).hexdigest()[:16]
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def declared_run_cost_units() -> int:
|
|
800
|
+
"""Return the per-run spend this deployment declares, in cost units. Zero when undeclared.
|
|
801
|
+
|
|
802
|
+
What this number is, stated plainly because the alternative reading is flattering and wrong:
|
|
803
|
+
it is a **declared budget, not a measurement**. This seam cannot observe what a run costs. It
|
|
804
|
+
hands the process to the worker with ``execve`` and never regains control, so there is no
|
|
805
|
+
moment here at which a real bill exists to record. What it can do is charge the monthly spend
|
|
806
|
+
ledger the amount the operator says one run is worth at worst, which is how admission-time
|
|
807
|
+
spend control works everywhere: reserve the worst case, admit only if it fits.
|
|
808
|
+
|
|
809
|
+
``MOSTLYRIGHT_RUN_COST_UNITS`` is therefore set on the worker job specification beside
|
|
810
|
+
``MOSTLYRIGHT_CAP_STORE``, and the two have the same character: they are not "on" and "slightly
|
|
811
|
+
less on".
|
|
812
|
+
|
|
813
|
+
- **Unset** — every run is charged zero, so the monthly spend ceiling in
|
|
814
|
+
:class:`governors.CapPolicy` cannot trip no matter how many runs are admitted. Spend is
|
|
815
|
+
bounded only by the cloud project's own budget ceiling. This is a real gap, not a
|
|
816
|
+
degraded-but-safe default. What still holds is the monthly run allowance, and only where
|
|
817
|
+
``MOSTLYRIGHT_CAP_STORE`` names shared storage (:func:`admission_ledger`); the concurrency
|
|
818
|
+
cap does not bind in this topology at all. With neither variable set, no cap here bounds
|
|
819
|
+
anything across executions.
|
|
820
|
+
- **Set to a whole number of cost units** — each admitted run reserves that much against the
|
|
821
|
+
monthly ceiling before it starts and commits the same amount when it does, so the ceiling
|
|
822
|
+
binds at ``max_monthly_cost_units / declared`` runs.
|
|
823
|
+
|
|
824
|
+
A value that cannot be read as a whole number of cost units refuses the run rather than
|
|
825
|
+
defaulting to zero, for the same reason an unreadable cap store refuses it: "this budget is
|
|
826
|
+
misconfigured" and "this run is free" must never be the same answer.
|
|
827
|
+
"""
|
|
828
|
+
|
|
829
|
+
configured = os.environ.get(RUN_COST_UNITS_ENV)
|
|
830
|
+
if configured is None or configured == "":
|
|
831
|
+
return 0
|
|
832
|
+
# str.isdigit is the check rather than int(): it refuses the signs, the underscores, the
|
|
833
|
+
# surrounding whitespace and the unicode digits that int() would otherwise accept and turn
|
|
834
|
+
# into a number nobody wrote.
|
|
835
|
+
if not configured.isascii() or not configured.isdigit():
|
|
836
|
+
raise HostedWorkerError(
|
|
837
|
+
"ADMISSION_REFUSED",
|
|
838
|
+
f"{RUN_COST_UNITS_ENV} must be a whole number of cost units, zero or more",
|
|
839
|
+
)
|
|
840
|
+
return int(configured)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _reserve_run_slot(ledger: CapLedger, job: Any, cost_units: int) -> Reservation:
|
|
844
|
+
"""Hold a slot for this run, or refuse the run before anything is allocated for it."""
|
|
845
|
+
|
|
846
|
+
coordinate = _tenant_coordinate(getattr(job, "workspace_id", None))
|
|
847
|
+
try:
|
|
848
|
+
return ledger.reserve(
|
|
849
|
+
coordinate,
|
|
850
|
+
now=datetime.now(UTC),
|
|
851
|
+
estimated_cost_units=cost_units,
|
|
852
|
+
)
|
|
853
|
+
except CapExceeded as error:
|
|
854
|
+
# A cap breach is the product working: it carries the cap that was reached and the two
|
|
855
|
+
# numbers behind it, in plain language, and no credential.
|
|
856
|
+
raise HostedWorkerError("ADMISSION_REFUSED", str(error)) from error
|
|
857
|
+
except CapStoreError as error:
|
|
858
|
+
# Fail closed. Counters that cannot be read are not counters that say zero.
|
|
859
|
+
raise HostedWorkerError("ADMISSION_REFUSED", str(error)) from error
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def launch(
|
|
863
|
+
config: BootstrapConfig,
|
|
864
|
+
*,
|
|
865
|
+
transport: BootstrapTransport | None = None,
|
|
866
|
+
execve: Callable[[str, Sequence[str], Mapping[str, str]], Any] = os.execve,
|
|
867
|
+
ledger: CapLedger | None = None,
|
|
868
|
+
_test_executable_resolver: Callable[[str], str] | None = None,
|
|
869
|
+
) -> NoReturn:
|
|
870
|
+
"""Exchange ambient identity, admit the run against its caps, and replace this process.
|
|
871
|
+
|
|
872
|
+
``ledger`` is a test seam and nothing else: when it is None the real admission ledger is built
|
|
873
|
+
here. The gate itself is unconditional. There is no flag that turns it off, because a cap a
|
|
874
|
+
caller can switch off is not a cap.
|
|
875
|
+
"""
|
|
876
|
+
|
|
877
|
+
if config.role not in {"producer", "verifier"}:
|
|
878
|
+
raise HostedWorkerError("BOOTSTRAP_CONFIG_INVALID", "role must be producer or verifier")
|
|
879
|
+
_v3_exchange_url(config.exchange_url)
|
|
880
|
+
_https_url(config.audience, "audience")
|
|
881
|
+
if config.worker_policy_binding is not None:
|
|
882
|
+
_require_local_policy_binding(config.worker_policy_binding)
|
|
883
|
+
client = transport or UrlLibBootstrapTransport()
|
|
884
|
+
identity = _metadata_identity(client, config.audience)
|
|
885
|
+
try:
|
|
886
|
+
response = _exchange(config, client, identity)
|
|
887
|
+
finally:
|
|
888
|
+
identity[:] = b"\x00" * len(identity)
|
|
889
|
+
raw_payloads, job = _validate_bootstrap_response(
|
|
890
|
+
response,
|
|
891
|
+
config.role,
|
|
892
|
+
worker_policy_binding=config.worker_policy_binding,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# The gate sits here, at the first moment the tenant coordinate is known and before anything
|
|
896
|
+
# is allocated for the run: a refused run creates no descriptor, holds no memory, and above
|
|
897
|
+
# all never reaches execve.
|
|
898
|
+
if ledger is None:
|
|
899
|
+
try:
|
|
900
|
+
ledger = admission_ledger()
|
|
901
|
+
except CapStoreError as error:
|
|
902
|
+
raise HostedWorkerError("ADMISSION_REFUSED", str(error)) from error
|
|
903
|
+
cost_units = declared_run_cost_units()
|
|
904
|
+
reservation = _reserve_run_slot(ledger, job, cost_units)
|
|
905
|
+
|
|
906
|
+
descriptors: dict[str, _AnonymousPayload] = {}
|
|
907
|
+
# Everything from here to the commit runs inside the reservation's block, so any fault —
|
|
908
|
+
# a descriptor that cannot be created, an entrypoint that fails validation — gives the slot
|
|
909
|
+
# back instead of leaving it held by a run that never started.
|
|
910
|
+
with reservation:
|
|
911
|
+
try:
|
|
912
|
+
for name, raw in raw_payloads.items():
|
|
913
|
+
mutable = bytearray(raw)
|
|
914
|
+
try:
|
|
915
|
+
descriptors[name] = _anonymous_payload(bytes(mutable), name)
|
|
916
|
+
finally:
|
|
917
|
+
mutable[:] = b"\x00" * len(mutable)
|
|
918
|
+
# Production has no executable-resolution injection seam. Tests may supply an explicit
|
|
919
|
+
# trusted stand-in because the fixed /opt/worker installation is created only in images.
|
|
920
|
+
executable = (
|
|
921
|
+
_worker_executable(config.role)
|
|
922
|
+
if _test_executable_resolver is None
|
|
923
|
+
else _test_executable_resolver(config.role)
|
|
924
|
+
)
|
|
925
|
+
arguments = _worker_argv(executable, config.role, descriptors)
|
|
926
|
+
environment = {
|
|
927
|
+
"PATH": "/opt/worker/bin:/usr/local/bin:/usr/bin:/bin",
|
|
928
|
+
"PYTHONDONTWRITEBYTECODE": "1",
|
|
929
|
+
"PYTHONUNBUFFERED": "1",
|
|
930
|
+
}
|
|
931
|
+
allowed = {payload.descriptor for payload in descriptors.values()}
|
|
932
|
+
changed = _restrict_inherited_descriptors(allowed)
|
|
933
|
+
try:
|
|
934
|
+
# Committed before the call, never after: execve replaces this process image and
|
|
935
|
+
# does not return, so an "after" that would record the run does not exist. A
|
|
936
|
+
# commit written below the call would never run and every admitted run would hold
|
|
937
|
+
# its slot forever. The committed amount is the declared budget rather than a
|
|
938
|
+
# measured bill for the same reason: after this line there is no process left here
|
|
939
|
+
# to measure anything with. See declared_run_cost_units.
|
|
940
|
+
reservation.commit(actual_cost_units=cost_units)
|
|
941
|
+
execve(executable, arguments, environment)
|
|
942
|
+
finally:
|
|
943
|
+
_restore_inherited_descriptors(changed)
|
|
944
|
+
finally:
|
|
945
|
+
for payload in descriptors.values():
|
|
946
|
+
payload.close()
|
|
947
|
+
raise HostedWorkerError("WORKER_EXEC_FAILED", "worker process replacement returned")
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
951
|
+
parser = argparse.ArgumentParser()
|
|
952
|
+
parser.add_argument("--exchange-url", required=True)
|
|
953
|
+
parser.add_argument("--audience", required=True)
|
|
954
|
+
parser.add_argument("--role", required=True, choices=("producer", "verifier"))
|
|
955
|
+
arguments = parser.parse_args(argv)
|
|
956
|
+
try:
|
|
957
|
+
launch(
|
|
958
|
+
BootstrapConfig(
|
|
959
|
+
exchange_url=arguments.exchange_url,
|
|
960
|
+
audience=arguments.audience,
|
|
961
|
+
role=arguments.role,
|
|
962
|
+
worker_policy_binding=_binding_from_environment(os.environ),
|
|
963
|
+
)
|
|
964
|
+
)
|
|
965
|
+
except HostedWorkerError as error:
|
|
966
|
+
print(f"{error.code}: {error.detail}", file=sys.stderr)
|
|
967
|
+
return 1
|
|
968
|
+
return 0
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
if __name__ == "__main__":
|
|
972
|
+
raise SystemExit(main())
|