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,356 @@
|
|
|
1
|
+
"""Capability-free Cloud Run Job bootstrap for one resumable ingestion delivery.
|
|
2
|
+
|
|
3
|
+
This is deliberately a small front door, not another worker protocol. The Job receives exactly
|
|
4
|
+
an opaque dispatch id and the Studio endpoint/audience derived from its image configuration. Its
|
|
5
|
+
ambient workload identity redeems that id once. All durable progress is represented by the
|
|
6
|
+
generation- and digest-pinned manifest reference in :mod:`hosted_ingestion_contract`; a process
|
|
7
|
+
that reaches its checkpoint reserve exits before credential expiry rather than trying to stretch a
|
|
8
|
+
delivery lease.
|
|
9
|
+
|
|
10
|
+
There is no generic source fetcher in this module. The only admitted first-slice continuation is
|
|
11
|
+
the immutable-public deterministic offset contract, and a reviewed connector injects its bounded
|
|
12
|
+
processor through :func:`run_hosted_ingestion_delivery`. This keeps an unwired Cloud Run Job from
|
|
13
|
+
silently treating an arbitrary URL or cursor as executable work.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from collections.abc import Callable, Sequence
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from datetime import UTC, datetime
|
|
24
|
+
from http import HTTPStatus
|
|
25
|
+
from typing import Protocol
|
|
26
|
+
from uuid import UUID
|
|
27
|
+
|
|
28
|
+
from mostlyright.data_harness.canonical import CanonicalJSONError, canonical_json_bytes, parse_json
|
|
29
|
+
from mostlyright.data_harness.hosted_bootstrap import (
|
|
30
|
+
BootstrapTransport,
|
|
31
|
+
UrlLibBootstrapTransport,
|
|
32
|
+
_https_url,
|
|
33
|
+
_metadata_identity,
|
|
34
|
+
)
|
|
35
|
+
from mostlyright.data_harness.hosted_ingestion_contract import (
|
|
36
|
+
HOSTED_INGESTION_DISPATCH_SCHEMA,
|
|
37
|
+
MAX_DELIVERY_DEADLINE_SECONDS,
|
|
38
|
+
HostedIngestionDispatchV1,
|
|
39
|
+
parse_delivery_deadline,
|
|
40
|
+
)
|
|
41
|
+
from mostlyright.data_harness.ingestion.contracts import IngestionError
|
|
42
|
+
|
|
43
|
+
INGESTION_DISPATCH_SCHEMA_VERSION = "3.0.0"
|
|
44
|
+
INGESTION_DISPATCH_EXCHANGE_PATH_TEMPLATE = (
|
|
45
|
+
"/internal/v3/ingestion/dispatches/{dispatch_id}:exchange"
|
|
46
|
+
)
|
|
47
|
+
MAX_INGESTION_DISPATCH_RESPONSE_BYTES = 64 * 1024
|
|
48
|
+
CHECKPOINT_RESERVE_SECONDS = 30.0
|
|
49
|
+
MAX_JOB_DEADLINE_SECONDS = 15 * 60
|
|
50
|
+
CONSUMED_INGESTION_DISPATCH_CODES = frozenset(
|
|
51
|
+
{"INGESTION_DISPATCH_ALREADY_CLAIMED", "INGESTION_DISPATCH_CLOSED"}
|
|
52
|
+
)
|
|
53
|
+
_CODE = re.compile(r"[A-Z][A-Z0-9_]{2,127}\Z")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class HostedIngestionJobError(RuntimeError):
|
|
57
|
+
"""A coded refusal which is safe to emit from the Job entrypoint."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, code: str, detail: str) -> None:
|
|
60
|
+
super().__init__(f"{code}: {detail}")
|
|
61
|
+
self.code = code
|
|
62
|
+
self.detail = detail
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class IngestionDispatch:
|
|
67
|
+
"""Exactly the three opaque/non-secret task arguments permitted by deployment."""
|
|
68
|
+
|
|
69
|
+
dispatch_id: str
|
|
70
|
+
exchange_url: str
|
|
71
|
+
audience: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class IngestionJobConfig:
|
|
76
|
+
"""The pinned Studio origin; storage authority stays out of this bootstrap config."""
|
|
77
|
+
|
|
78
|
+
studio_origin: str
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_environment(cls, environment: dict[str, str] | None = None) -> IngestionJobConfig:
|
|
82
|
+
active = os.environ if environment is None else environment
|
|
83
|
+
origin = active.get("STUDIO_API_ORIGIN")
|
|
84
|
+
if not isinstance(origin, str) or not origin:
|
|
85
|
+
raise HostedIngestionJobError(
|
|
86
|
+
"INGESTION_DISPATCH_CONFIG_INVALID", "STUDIO_API_ORIGIN is required"
|
|
87
|
+
)
|
|
88
|
+
try:
|
|
89
|
+
normalized = _https_url(origin, "STUDIO_API_ORIGIN")
|
|
90
|
+
except (RuntimeError, ValueError) as error:
|
|
91
|
+
raise HostedIngestionJobError(
|
|
92
|
+
"INGESTION_DISPATCH_CONFIG_INVALID", "STUDIO_API_ORIGIN is not HTTPS"
|
|
93
|
+
) from error
|
|
94
|
+
return cls(studio_origin=normalized.rstrip("/"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _uuid4(value: object) -> str:
|
|
98
|
+
if not isinstance(value, str):
|
|
99
|
+
raise HostedIngestionJobError("INGESTION_DISPATCH_INVALID", "dispatch id must be a UUID4")
|
|
100
|
+
try:
|
|
101
|
+
parsed = UUID(value)
|
|
102
|
+
except ValueError as error:
|
|
103
|
+
raise HostedIngestionJobError(
|
|
104
|
+
"INGESTION_DISPATCH_INVALID", "dispatch id must be a UUID4"
|
|
105
|
+
) from error
|
|
106
|
+
if parsed.version != 4 or str(parsed) != value:
|
|
107
|
+
raise HostedIngestionJobError("INGESTION_DISPATCH_INVALID", "dispatch id must be a UUID4")
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _https(value: object, label: str) -> str:
|
|
112
|
+
try:
|
|
113
|
+
return _https_url(value, label)
|
|
114
|
+
except (RuntimeError, TypeError, ValueError) as error:
|
|
115
|
+
raise HostedIngestionJobError(
|
|
116
|
+
"INGESTION_DISPATCH_INVALID", f"{label} is not HTTPS"
|
|
117
|
+
) from error
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parse_ingestion_dispatch(argv: Sequence[str]) -> IngestionDispatch:
|
|
121
|
+
"""Parse the only six argv cells deployment may put on an ingestion Job task."""
|
|
122
|
+
|
|
123
|
+
values = tuple(argv)
|
|
124
|
+
flags = (
|
|
125
|
+
"--ingestion-dispatch-id",
|
|
126
|
+
"--ingestion-dispatch-exchange-url",
|
|
127
|
+
"--ingestion-dispatch-audience",
|
|
128
|
+
)
|
|
129
|
+
if len(values) != 6 or values[::2] != flags or any(not value for value in values[1::2]):
|
|
130
|
+
raise HostedIngestionJobError(
|
|
131
|
+
"INGESTION_DISPATCH_ARGUMENTS_INVALID",
|
|
132
|
+
"expected --ingestion-dispatch-id, --ingestion-dispatch-exchange-url and "
|
|
133
|
+
"--ingestion-dispatch-audience in that order",
|
|
134
|
+
)
|
|
135
|
+
dispatch_id, exchange_url, audience = values[1::2]
|
|
136
|
+
return IngestionDispatch(
|
|
137
|
+
dispatch_id=_uuid4(dispatch_id),
|
|
138
|
+
exchange_url=_https(exchange_url, "ingestion dispatch exchange URL"),
|
|
139
|
+
audience=_https(audience, "ingestion dispatch audience"),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _validate_dispatch(config: IngestionJobConfig, dispatch: IngestionDispatch) -> None:
|
|
144
|
+
expected = config.studio_origin + INGESTION_DISPATCH_EXCHANGE_PATH_TEMPLATE.format(
|
|
145
|
+
dispatch_id=dispatch.dispatch_id
|
|
146
|
+
)
|
|
147
|
+
if dispatch.exchange_url != expected:
|
|
148
|
+
raise HostedIngestionJobError(
|
|
149
|
+
"INGESTION_DISPATCH_URL_UNRECOGNIZED",
|
|
150
|
+
"ingestion dispatch exchange URL is not this deployment's endpoint for this dispatch",
|
|
151
|
+
)
|
|
152
|
+
if dispatch.audience != config.studio_origin:
|
|
153
|
+
raise HostedIngestionJobError(
|
|
154
|
+
"INGESTION_DISPATCH_AUDIENCE_UNRECOGNIZED",
|
|
155
|
+
"ingestion dispatch audience is not this deployment's Studio audience",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _exchange_error(status: int, raw: bytearray) -> HostedIngestionJobError:
|
|
160
|
+
try:
|
|
161
|
+
document = parse_json(bytes(raw))
|
|
162
|
+
except (CanonicalJSONError, TypeError, ValueError):
|
|
163
|
+
document = None
|
|
164
|
+
if isinstance(document, dict) and isinstance(document.get("code"), str):
|
|
165
|
+
code = document["code"]
|
|
166
|
+
if _CODE.fullmatch(code) is not None:
|
|
167
|
+
return HostedIngestionJobError(
|
|
168
|
+
code, f"ingestion dispatch exchange returned HTTP {status}"
|
|
169
|
+
)
|
|
170
|
+
return HostedIngestionJobError(
|
|
171
|
+
"INGESTION_DISPATCH_EXCHANGE_FAILED",
|
|
172
|
+
f"ingestion dispatch exchange returned HTTP {status}",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def exchange_ingestion_dispatch(
|
|
177
|
+
*,
|
|
178
|
+
config: IngestionJobConfig,
|
|
179
|
+
dispatch: IngestionDispatch,
|
|
180
|
+
transport: BootstrapTransport | None = None,
|
|
181
|
+
) -> HostedIngestionDispatchV1:
|
|
182
|
+
"""Redeem one opaque id through workload identity and parse the closed response document."""
|
|
183
|
+
|
|
184
|
+
_validate_dispatch(config, dispatch)
|
|
185
|
+
active = transport or UrlLibBootstrapTransport()
|
|
186
|
+
identity = _metadata_identity(active, dispatch.audience)
|
|
187
|
+
request = canonical_json_bytes(
|
|
188
|
+
{"schema_version": INGESTION_DISPATCH_SCHEMA_VERSION, "dispatch_id": dispatch.dispatch_id}
|
|
189
|
+
)
|
|
190
|
+
try:
|
|
191
|
+
try:
|
|
192
|
+
token = identity.decode("ascii")
|
|
193
|
+
except UnicodeDecodeError as error: # pragma: no cover - metadata helper validates this
|
|
194
|
+
raise HostedIngestionJobError(
|
|
195
|
+
"INGESTION_DISPATCH_IDENTITY_INVALID", "workload identity was not ASCII"
|
|
196
|
+
) from error
|
|
197
|
+
status, response = active.request(
|
|
198
|
+
"POST",
|
|
199
|
+
dispatch.exchange_url,
|
|
200
|
+
{"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
|
201
|
+
request,
|
|
202
|
+
MAX_INGESTION_DISPATCH_RESPONSE_BYTES,
|
|
203
|
+
)
|
|
204
|
+
finally:
|
|
205
|
+
identity[:] = b"\x00" * len(identity)
|
|
206
|
+
raw = bytearray(response)
|
|
207
|
+
try:
|
|
208
|
+
if status != HTTPStatus.OK:
|
|
209
|
+
raise _exchange_error(status, raw)
|
|
210
|
+
try:
|
|
211
|
+
document = parse_json(bytes(raw))
|
|
212
|
+
result = HostedIngestionDispatchV1.from_dict(document)
|
|
213
|
+
except (ValueError, IngestionError, TypeError) as error:
|
|
214
|
+
raise HostedIngestionJobError(
|
|
215
|
+
"INGESTION_DISPATCH_RESPONSE_INVALID",
|
|
216
|
+
"ingestion dispatch exchange response is not the exact accepted schema",
|
|
217
|
+
) from error
|
|
218
|
+
if result.schema_version != HOSTED_INGESTION_DISPATCH_SCHEMA:
|
|
219
|
+
raise AssertionError("HostedIngestionDispatchV1 accepted an unsupported schema")
|
|
220
|
+
return result
|
|
221
|
+
finally:
|
|
222
|
+
raw[:] = b"\x00" * len(raw)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True)
|
|
226
|
+
class DeliveryDeadline:
|
|
227
|
+
"""A monotonic checkpoint boundary derived from Studio's absolute UTC deadline."""
|
|
228
|
+
|
|
229
|
+
deadline: datetime
|
|
230
|
+
wall_clock: Callable[[], datetime] = lambda: datetime.now(UTC)
|
|
231
|
+
reserve_seconds: float = CHECKPOINT_RESERVE_SECONDS
|
|
232
|
+
|
|
233
|
+
def __post_init__(self) -> None:
|
|
234
|
+
if self.deadline.tzinfo is None:
|
|
235
|
+
raise ValueError("deadline must be timezone aware")
|
|
236
|
+
if not 0 < self.reserve_seconds < MAX_DELIVERY_DEADLINE_SECONDS:
|
|
237
|
+
raise ValueError("checkpoint reserve is invalid")
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def from_dispatch(
|
|
241
|
+
cls,
|
|
242
|
+
dispatch: HostedIngestionDispatchV1,
|
|
243
|
+
*,
|
|
244
|
+
wall_clock: Callable[[], datetime] = lambda: datetime.now(UTC),
|
|
245
|
+
reserve_seconds: float = CHECKPOINT_RESERVE_SECONDS,
|
|
246
|
+
) -> DeliveryDeadline:
|
|
247
|
+
deadline = parse_delivery_deadline(dispatch.delivery.deadline)
|
|
248
|
+
remaining = (deadline - wall_clock().astimezone(UTC)).total_seconds()
|
|
249
|
+
if remaining <= reserve_seconds:
|
|
250
|
+
raise HostedIngestionJobError(
|
|
251
|
+
"INGESTION_DELIVERY_EXPIRED", "delivery has no safe checkpoint window remaining"
|
|
252
|
+
)
|
|
253
|
+
if remaining > MAX_JOB_DEADLINE_SECONDS:
|
|
254
|
+
raise HostedIngestionJobError(
|
|
255
|
+
"INGESTION_DELIVERY_DEADLINE_INVALID", "delivery deadline exceeds the Job ceiling"
|
|
256
|
+
)
|
|
257
|
+
return cls(deadline=deadline, wall_clock=wall_clock, reserve_seconds=reserve_seconds)
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def checkpoint_at(self) -> datetime:
|
|
261
|
+
from datetime import timedelta
|
|
262
|
+
|
|
263
|
+
return self.deadline - timedelta(seconds=self.reserve_seconds)
|
|
264
|
+
|
|
265
|
+
def checkpoint_due(self) -> bool:
|
|
266
|
+
return self.wall_clock().astimezone(UTC) >= self.checkpoint_at
|
|
267
|
+
|
|
268
|
+
def require_work_time(self) -> None:
|
|
269
|
+
if self.checkpoint_due():
|
|
270
|
+
raise CheckpointRequired()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class CheckpointRequired(RuntimeError):
|
|
274
|
+
"""Normal delivery exit: durable state must be handed back before capability expiry."""
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class DeliveryProcessor(Protocol):
|
|
278
|
+
"""A reviewed connector's bounded execution hook, intentionally not a generic URL runner."""
|
|
279
|
+
|
|
280
|
+
def __call__(
|
|
281
|
+
self, dispatch: HostedIngestionDispatchV1, deadline: DeliveryDeadline
|
|
282
|
+
) -> DeliveryOutcome: ...
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass(frozen=True)
|
|
286
|
+
class DeliveryOutcome:
|
|
287
|
+
"""A connector tells Studio's future callback layer whether it finalized or checkpointed."""
|
|
288
|
+
|
|
289
|
+
status: str
|
|
290
|
+
|
|
291
|
+
def __post_init__(self) -> None:
|
|
292
|
+
if self.status not in {"checkpointed", "finalized"}:
|
|
293
|
+
raise ValueError("delivery status must be checkpointed or finalized")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def run_hosted_ingestion_delivery(
|
|
297
|
+
*, dispatch: HostedIngestionDispatchV1, processor: DeliveryProcessor, deadline: DeliveryDeadline
|
|
298
|
+
) -> DeliveryOutcome:
|
|
299
|
+
"""Run one reviewed processor but force a normal checkpoint exit before its hard deadline."""
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
deadline.require_work_time()
|
|
303
|
+
outcome = processor(dispatch, deadline)
|
|
304
|
+
except CheckpointRequired:
|
|
305
|
+
return DeliveryOutcome("checkpointed")
|
|
306
|
+
if not isinstance(outcome, DeliveryOutcome):
|
|
307
|
+
raise HostedIngestionJobError(
|
|
308
|
+
"INGESTION_DELIVERY_PROCESSOR_INVALID", "connector returned no bounded delivery outcome"
|
|
309
|
+
)
|
|
310
|
+
if deadline.checkpoint_due() and outcome.status != "checkpointed":
|
|
311
|
+
raise HostedIngestionJobError(
|
|
312
|
+
"INGESTION_DELIVERY_DEADLINE_OVERRUN",
|
|
313
|
+
"connector returned after the checkpoint boundary without checkpointing",
|
|
314
|
+
)
|
|
315
|
+
return outcome
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def ingestion_job_main(argv: Sequence[str] | None = None) -> int:
|
|
319
|
+
"""Cloud Run entrypoint. It refuses safely until a reviewed connector is wired in."""
|
|
320
|
+
|
|
321
|
+
arguments = tuple(sys.argv[1:] if argv is None else argv)
|
|
322
|
+
try:
|
|
323
|
+
dispatch = parse_ingestion_dispatch(arguments)
|
|
324
|
+
except HostedIngestionJobError as error:
|
|
325
|
+
sys.stderr.write(f"{error.code}: {error.detail}\n")
|
|
326
|
+
return 2
|
|
327
|
+
try:
|
|
328
|
+
config = IngestionJobConfig.from_environment()
|
|
329
|
+
delivery = exchange_ingestion_dispatch(config=config, dispatch=dispatch)
|
|
330
|
+
DeliveryDeadline.from_dispatch(delivery)
|
|
331
|
+
except HostedIngestionJobError as error:
|
|
332
|
+
if error.code in CONSUMED_INGESTION_DISPATCH_CODES:
|
|
333
|
+
return 0
|
|
334
|
+
sys.stderr.write(f"{error.code}: {error.detail}\n")
|
|
335
|
+
return 1
|
|
336
|
+
# The deliberately closed dispatch contract has no source URL, generic callback endpoint or
|
|
337
|
+
# credential. A later reviewed connector/callback slice must call run_hosted_ingestion_delivery
|
|
338
|
+
# instead of making this entrypoint silently invent one of those authorities.
|
|
339
|
+
sys.stderr.write("INGESTION_DELIVERY_PROCESSOR_UNWIRED: no reviewed connector is installed\n")
|
|
340
|
+
return 1
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
__all__ = [
|
|
344
|
+
"CHECKPOINT_RESERVE_SECONDS",
|
|
345
|
+
"CONSUMED_INGESTION_DISPATCH_CODES",
|
|
346
|
+
"CheckpointRequired",
|
|
347
|
+
"DeliveryDeadline",
|
|
348
|
+
"DeliveryOutcome",
|
|
349
|
+
"HostedIngestionJobError",
|
|
350
|
+
"IngestionDispatch",
|
|
351
|
+
"IngestionJobConfig",
|
|
352
|
+
"exchange_ingestion_dispatch",
|
|
353
|
+
"ingestion_job_main",
|
|
354
|
+
"parse_ingestion_dispatch",
|
|
355
|
+
"run_hosted_ingestion_delivery",
|
|
356
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Offline smoke for the identity-only resumable-ingestion Cloud Run Job image entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from mostlyright.data_harness.hosted_ingestion_job import parse_ingestion_dispatch
|
|
10
|
+
|
|
11
|
+
_DISPATCH_ID = "99999999-9999-4999-8999-999999999999"
|
|
12
|
+
_JOB_ARGS = (
|
|
13
|
+
"--ingestion-dispatch-id",
|
|
14
|
+
_DISPATCH_ID,
|
|
15
|
+
"--ingestion-dispatch-exchange-url",
|
|
16
|
+
f"https://studio.invalid/internal/v3/ingestion/dispatches/{_DISPATCH_ID}:exchange",
|
|
17
|
+
"--ingestion-dispatch-audience",
|
|
18
|
+
"https://studio.invalid",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> int:
|
|
23
|
+
dispatch = parse_ingestion_dispatch(_JOB_ARGS)
|
|
24
|
+
if dispatch.dispatch_id != _DISPATCH_ID:
|
|
25
|
+
raise SystemExit("ingestion Job parser did not preserve the dispatch id")
|
|
26
|
+
entrypoint = Path(sys.executable).with_name("mr-data-ingestion-job")
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
[entrypoint, "--not-an-ingestion-job-argument"],
|
|
29
|
+
check=False,
|
|
30
|
+
capture_output=True,
|
|
31
|
+
timeout=10,
|
|
32
|
+
)
|
|
33
|
+
if result.returncode != 2 or b"INGESTION_DISPATCH_ARGUMENTS_INVALID" not in result.stderr:
|
|
34
|
+
raise SystemExit("ingestion Job entrypoint does not enforce its exact argv contract")
|
|
35
|
+
print("hosted-ingestion-job-smoke-ok")
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__": # pragma: no cover - executed in the built image
|
|
40
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Linux image smoke for the production research-session parser boundaries.
|
|
2
|
+
|
|
3
|
+
This module is run inside the real session-worker image with Docker's memory ceiling set to the
|
|
4
|
+
exact Cloud Run value. Unit tests can exercise every refusal, but only this lane proves the image's
|
|
5
|
+
non-root entrypoint sees the real cgroup spelling and can install seccomp before the installed
|
|
6
|
+
parser execs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import signal
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from mostlyright.data_harness.acquisition.parsing import ParseLimits
|
|
19
|
+
from mostlyright.data_harness.acquisition.sandbox import (
|
|
20
|
+
HOSTED_SESSION_CONTAINER_MEMORY_BYTES,
|
|
21
|
+
_run_worker_command,
|
|
22
|
+
)
|
|
23
|
+
from mostlyright.data_harness.hosted_session_worker import SessionWorkerConfig, _default_sandbox
|
|
24
|
+
|
|
25
|
+
_ADVERSARIAL_CHILD = r"""
|
|
26
|
+
import ctypes
|
|
27
|
+
import errno
|
|
28
|
+
import os
|
|
29
|
+
import socket
|
|
30
|
+
|
|
31
|
+
status = open("/proc/self/status", encoding="ascii").read()
|
|
32
|
+
if "NoNewPrivs:\t1" not in status or "Seccomp:\t2" not in status:
|
|
33
|
+
raise SystemExit(20)
|
|
34
|
+
|
|
35
|
+
def memory_limit():
|
|
36
|
+
for candidate in (
|
|
37
|
+
"/sys/fs/cgroup/memory.max",
|
|
38
|
+
"/sys/fs/cgroup/memory/memory.limit_in_bytes",
|
|
39
|
+
):
|
|
40
|
+
try:
|
|
41
|
+
return open(candidate, encoding="ascii").read().strip()
|
|
42
|
+
except OSError:
|
|
43
|
+
continue
|
|
44
|
+
raise SystemExit(21)
|
|
45
|
+
|
|
46
|
+
if memory_limit() != "4294967296":
|
|
47
|
+
raise SystemExit(22)
|
|
48
|
+
try:
|
|
49
|
+
open(f"/proc/{os.getppid()}/environ", "rb").read()
|
|
50
|
+
except PermissionError:
|
|
51
|
+
pass
|
|
52
|
+
else:
|
|
53
|
+
raise SystemExit(23)
|
|
54
|
+
membership = open("/proc/self/cgroup", encoding="ascii").read()
|
|
55
|
+
child = os.fork()
|
|
56
|
+
if child == 0:
|
|
57
|
+
if open("/proc/self/cgroup", encoding="ascii").read() != membership:
|
|
58
|
+
os._exit(30)
|
|
59
|
+
if memory_limit() != "4294967296":
|
|
60
|
+
os._exit(31)
|
|
61
|
+
try:
|
|
62
|
+
socket.socket()
|
|
63
|
+
except OSError as error:
|
|
64
|
+
os._exit(0 if error.errno == errno.EPERM else 32)
|
|
65
|
+
os._exit(33)
|
|
66
|
+
_, child_status = os.waitpid(child, 0)
|
|
67
|
+
if os.waitstatus_to_exitcode(child_status) != 0:
|
|
68
|
+
raise SystemExit(34)
|
|
69
|
+
|
|
70
|
+
libc = ctypes.CDLL(None, use_errno=True)
|
|
71
|
+
for number, arguments in (
|
|
72
|
+
(101, (0, 0, 0, 0)),
|
|
73
|
+
(109, (0, 0)),
|
|
74
|
+
(112, ()),
|
|
75
|
+
(272, (0,)),
|
|
76
|
+
(308, (-1, 0)),
|
|
77
|
+
(310, (os.getppid(), 0, 0, 0, 0, 0)),
|
|
78
|
+
(311, (os.getppid(), 0, 0, 0, 0, 0)),
|
|
79
|
+
(434, (os.getppid(), 0)),
|
|
80
|
+
(438, (-1, 0, 0)),
|
|
81
|
+
):
|
|
82
|
+
ctypes.set_errno(0)
|
|
83
|
+
outcome = libc.syscall(number, *arguments)
|
|
84
|
+
if outcome != -1 or ctypes.get_errno() != errno.EPERM:
|
|
85
|
+
raise SystemExit(35)
|
|
86
|
+
ctypes.set_errno(0)
|
|
87
|
+
if libc.syscall(435, 0, 0) != -1 or ctypes.get_errno() != errno.ENOSYS:
|
|
88
|
+
raise SystemExit(36)
|
|
89
|
+
print("hosted-session-container-boundary-ok")
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
_LINGERING_DESCENDANT_CHILD = r"""
|
|
93
|
+
import errno
|
|
94
|
+
import os
|
|
95
|
+
import signal
|
|
96
|
+
|
|
97
|
+
descendant = os.fork()
|
|
98
|
+
if descendant == 0:
|
|
99
|
+
for detach in (os.setsid, lambda: os.setpgid(0, 0)):
|
|
100
|
+
try:
|
|
101
|
+
detach()
|
|
102
|
+
except OSError as error:
|
|
103
|
+
if error.errno != errno.EPERM:
|
|
104
|
+
os._exit(40)
|
|
105
|
+
else:
|
|
106
|
+
os._exit(41)
|
|
107
|
+
for descriptor in (0, 1, 2):
|
|
108
|
+
try:
|
|
109
|
+
os.close(descriptor)
|
|
110
|
+
except OSError:
|
|
111
|
+
pass
|
|
112
|
+
while True:
|
|
113
|
+
signal.pause()
|
|
114
|
+
print(descendant, flush=True)
|
|
115
|
+
os._exit(0)
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _require_descendant_reaped(pid: int) -> None:
|
|
120
|
+
deadline = time.monotonic() + 5.0
|
|
121
|
+
while time.monotonic() < deadline:
|
|
122
|
+
try:
|
|
123
|
+
waited, status = os.waitpid(pid, os.WNOHANG)
|
|
124
|
+
except ChildProcessError:
|
|
125
|
+
waited = 0
|
|
126
|
+
status = 0
|
|
127
|
+
if waited == pid:
|
|
128
|
+
if not os.WIFSIGNALED(status) or os.WTERMSIG(status) != signal.SIGKILL:
|
|
129
|
+
raise SystemExit("hosted session descendant did not die with group cleanup")
|
|
130
|
+
return
|
|
131
|
+
try:
|
|
132
|
+
os.kill(pid, 0)
|
|
133
|
+
except ProcessLookupError:
|
|
134
|
+
return
|
|
135
|
+
time.sleep(0.01)
|
|
136
|
+
raise SystemExit("hosted session descendant survived its request")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def main() -> int:
|
|
140
|
+
if sys.platform != "linux":
|
|
141
|
+
raise SystemExit("hosted session container smoke requires Linux")
|
|
142
|
+
config = SessionWorkerConfig(
|
|
143
|
+
studio_origin="https://studio.invalid",
|
|
144
|
+
lease_audience="https://studio.invalid",
|
|
145
|
+
warm_pool_size=0,
|
|
146
|
+
)
|
|
147
|
+
sandbox = _default_sandbox(config)
|
|
148
|
+
result = sandbox.parse(
|
|
149
|
+
request_id="session.container.smoke",
|
|
150
|
+
content=b"city,temp\noslo,1\n",
|
|
151
|
+
data_format="csv",
|
|
152
|
+
media_type="text/csv",
|
|
153
|
+
filename="source.csv",
|
|
154
|
+
limits=ParseLimits(),
|
|
155
|
+
)
|
|
156
|
+
if result.parsed.columns != ("city", "temp") or result.parsed.rows != (("oslo", "1"),):
|
|
157
|
+
raise SystemExit("installed session parser returned the wrong table")
|
|
158
|
+
|
|
159
|
+
with tempfile.TemporaryDirectory(prefix="mr-session-container-smoke-") as temporary:
|
|
160
|
+
stdout, stderr, returncode = _run_worker_command(
|
|
161
|
+
[sys.executable, "-I", "-c", _ADVERSARIAL_CHILD],
|
|
162
|
+
cwd=Path(temporary).resolve(),
|
|
163
|
+
env={"PATH": os.defpath, "PYTHONIOENCODING": "utf-8"},
|
|
164
|
+
request_bytes=b"",
|
|
165
|
+
timeout_seconds=10.0,
|
|
166
|
+
memory_cgroup_root_fd=None,
|
|
167
|
+
hosted_session_container_memory_bytes=HOSTED_SESSION_CONTAINER_MEMORY_BYTES,
|
|
168
|
+
)
|
|
169
|
+
if returncode != 0 or stderr or stdout.strip() != b"hosted-session-container-boundary-ok":
|
|
170
|
+
raise SystemExit("installed session child escaped or lacked its production boundaries")
|
|
171
|
+
|
|
172
|
+
with tempfile.TemporaryDirectory(prefix="mr-session-descendant-smoke-") as temporary:
|
|
173
|
+
stdout, stderr, returncode = _run_worker_command(
|
|
174
|
+
[sys.executable, "-I", "-c", _LINGERING_DESCENDANT_CHILD],
|
|
175
|
+
cwd=Path(temporary).resolve(),
|
|
176
|
+
env={"PATH": os.defpath, "PYTHONIOENCODING": "utf-8"},
|
|
177
|
+
request_bytes=b"",
|
|
178
|
+
timeout_seconds=10.0,
|
|
179
|
+
memory_cgroup_root_fd=None,
|
|
180
|
+
hosted_session_container_memory_bytes=HOSTED_SESSION_CONTAINER_MEMORY_BYTES,
|
|
181
|
+
)
|
|
182
|
+
try:
|
|
183
|
+
descendant_pid = int(stdout.strip())
|
|
184
|
+
except ValueError as error:
|
|
185
|
+
raise SystemExit("hosted session descendant did not report its pid") from error
|
|
186
|
+
if returncode != 0 or stderr:
|
|
187
|
+
raise SystemExit("hosted session descendant could detach before cleanup")
|
|
188
|
+
_require_descendant_reaped(descendant_pid)
|
|
189
|
+
print("hosted-session-container-smoke-ok")
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
raise SystemExit(main())
|