fix(simulation): require gaussian runtime provenance
This commit is contained in:
@@ -22,6 +22,8 @@ RESULT_SCHEMA: Final = "gaussian-pipeline.build-result/v1"
|
|||||||
CAPABILITIES_SCHEMA: Final = "gaussian-pipeline.capabilities/v1"
|
CAPABILITIES_SCHEMA: Final = "gaussian-pipeline.capabilities/v1"
|
||||||
SAFE_UPLOAD_ID: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
SAFE_UPLOAD_ID: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
||||||
SHA256_PATTERN: Final = re.compile(r"^[a-f0-9]{64}$")
|
SHA256_PATTERN: Final = re.compile(r"^[a-f0-9]{64}$")
|
||||||
|
SOURCE_REVISION_PATTERN: Final = re.compile(r"^[a-f0-9]{40}$")
|
||||||
|
IMAGE_DIGEST_PATTERN: Final = re.compile(r"^sha256:[a-f0-9]{64}$")
|
||||||
DEFAULT_CHUNK_BYTES: Final = 8 * 1024 * 1024
|
DEFAULT_CHUNK_BYTES: Final = 8 * 1024 * 1024
|
||||||
MAX_JSON_RESPONSE_BYTES: Final = 32 * 1024 * 1024
|
MAX_JSON_RESPONSE_BYTES: Final = 32 * 1024 * 1024
|
||||||
MAX_RETRIES: Final = 3
|
MAX_RETRIES: Final = 3
|
||||||
@@ -103,8 +105,10 @@ class GaussianPipelineGateway:
|
|||||||
if (
|
if (
|
||||||
document.get("schema_version") != CAPABILITIES_SCHEMA
|
document.get("schema_version") != CAPABILITIES_SCHEMA
|
||||||
or document.get("service") != "ndc-gaussian-pipeline"
|
or document.get("service") != "ndc-gaussian-pipeline"
|
||||||
|
or document.get("api_version") != "gaussian-pipeline.api/v1"
|
||||||
):
|
):
|
||||||
raise GaussianPipelineGatewayError("Gaussian provider capabilities do not match v1")
|
raise GaussianPipelineGatewayError("Gaussian provider capabilities do not match v1")
|
||||||
|
_validate_runtime_provenance(document)
|
||||||
return document
|
return document
|
||||||
|
|
||||||
def upload_source(
|
def upload_source(
|
||||||
@@ -185,6 +189,7 @@ class GaussianPipelineGateway:
|
|||||||
result = self._json("GET", f"/v1/jobs/{quote(job_id, safe='')}/result")
|
result = self._json("GET", f"/v1/jobs/{quote(job_id, safe='')}/result")
|
||||||
if result.get("schema_version") != RESULT_SCHEMA or result.get("job_id") != job_id:
|
if result.get("schema_version") != RESULT_SCHEMA or result.get("job_id") != job_id:
|
||||||
raise GaussianPipelineGatewayError("Gaussian result identity does not match")
|
raise GaussianPipelineGatewayError("Gaussian result identity does not match")
|
||||||
|
_validate_runtime_provenance(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def download_artifact(
|
def download_artifact(
|
||||||
@@ -408,6 +413,21 @@ def _safe_id(value: str, label: str) -> None:
|
|||||||
raise GaussianPipelineGatewayError(f"Gaussian {label} is invalid")
|
raise GaussianPipelineGatewayError(f"Gaussian {label} is invalid")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_runtime_provenance(document: Mapping[str, object]) -> None:
|
||||||
|
runtime = document.get("runtime")
|
||||||
|
if not isinstance(runtime, dict):
|
||||||
|
raise GaussianPipelineGatewayError("Gaussian runtime provenance is unavailable")
|
||||||
|
source_revision = runtime.get("source_revision")
|
||||||
|
image_digest = runtime.get("image_digest")
|
||||||
|
if (
|
||||||
|
not isinstance(source_revision, str)
|
||||||
|
or SOURCE_REVISION_PATTERN.fullmatch(source_revision) is None
|
||||||
|
or not isinstance(image_digest, str)
|
||||||
|
or IMAGE_DIGEST_PATTERN.fullmatch(image_digest) is None
|
||||||
|
):
|
||||||
|
raise GaussianPipelineGatewayError("Gaussian runtime provenance is invalid")
|
||||||
|
|
||||||
|
|
||||||
def _unavailable(message: str, error: httpx.HTTPError) -> GaussianPipelineGatewayError:
|
def _unavailable(message: str, error: httpx.HTTPError) -> GaussianPipelineGatewayError:
|
||||||
if isinstance(error, httpx.TransportError):
|
if isinstance(error, httpx.TransportError):
|
||||||
return GaussianPipelineUnavailableError(message)
|
return GaussianPipelineUnavailableError(message)
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ def test_gateway_uploads_with_tus_and_reads_provider_contract(tmp_path: Path) ->
|
|||||||
json={
|
json={
|
||||||
"schema_version": "gaussian-pipeline.capabilities/v1",
|
"schema_version": "gaussian-pipeline.capabilities/v1",
|
||||||
"service": "ndc-gaussian-pipeline",
|
"service": "ndc-gaussian-pipeline",
|
||||||
|
"api_version": "gaussian-pipeline.api/v1",
|
||||||
"provider": {"id": "playcanvas-splat-transform", "version": "3.3.3"},
|
"provider": {"id": "playcanvas-splat-transform", "version": "3.3.3"},
|
||||||
|
"runtime": {
|
||||||
|
"source_revision": "a" * 40,
|
||||||
|
"image_digest": f"sha256:{'b' * 64}",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if request.method == "POST" and request.url.path == "/v1/uploads":
|
if request.method == "POST" and request.url.path == "/v1/uploads":
|
||||||
@@ -177,7 +182,14 @@ def test_gateway_submits_tracks_and_imports_digest_bound_artifact(tmp_path: Path
|
|||||||
if request.method == "GET" and request.url.path == f"/v1/jobs/{job_id}/result":
|
if request.method == "GET" and request.url.path == f"/v1/jobs/{job_id}/result":
|
||||||
return httpx.Response(
|
return httpx.Response(
|
||||||
200,
|
200,
|
||||||
json={"schema_version": "gaussian-pipeline.build-result/v1", "job_id": job_id},
|
json={
|
||||||
|
"schema_version": "gaussian-pipeline.build-result/v1",
|
||||||
|
"job_id": job_id,
|
||||||
|
"runtime": {
|
||||||
|
"source_revision": "a" * 40,
|
||||||
|
"image_digest": f"sha256:{'b' * 64}",
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if request.method == "GET" and request.url.path.endswith("/artifacts/preview.sog"):
|
if request.method == "GET" and request.url.path.endswith("/artifacts/preview.sog"):
|
||||||
return httpx.Response(200, content=artifact)
|
return httpx.Response(200, content=artifact)
|
||||||
@@ -201,3 +213,25 @@ def test_gateway_submits_tracks_and_imports_digest_bound_artifact(tmp_path: Path
|
|||||||
tmp_path / "import" / "preview.sog",
|
tmp_path / "import" / "preview.sog",
|
||||||
)
|
)
|
||||||
assert destination.read_bytes() == artifact
|
assert destination.read_bytes() == artifact
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_rejects_capabilities_without_runtime_provenance(tmp_path: Path) -> None:
|
||||||
|
def handler(_request: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"schema_version": "gaussian-pipeline.capabilities/v1",
|
||||||
|
"service": "ndc-gaussian-pipeline",
|
||||||
|
"api_version": "gaussian-pipeline.api/v1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
GaussianPipelineGateway(
|
||||||
|
"http://gaussian.test",
|
||||||
|
_token_file(tmp_path),
|
||||||
|
transport=httpx.MockTransport(handler),
|
||||||
|
) as gateway,
|
||||||
|
pytest.raises(GaussianPipelineGatewayError, match="provenance is unavailable"),
|
||||||
|
):
|
||||||
|
gateway.capabilities()
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ def _gateway(tmp_path: Path, status: int = 200) -> GaussianPipelineGateway:
|
|||||||
json={
|
json={
|
||||||
"schema_version": "gaussian-pipeline.capabilities/v1",
|
"schema_version": "gaussian-pipeline.capabilities/v1",
|
||||||
"service": "ndc-gaussian-pipeline",
|
"service": "ndc-gaussian-pipeline",
|
||||||
|
"api_version": "gaussian-pipeline.api/v1",
|
||||||
"provider": {"id": "playcanvas-splat-transform", "version": "3.3.3"},
|
"provider": {"id": "playcanvas-splat-transform", "version": "3.3.3"},
|
||||||
|
"runtime": {
|
||||||
|
"source_revision": "a" * 40,
|
||||||
|
"image_digest": f"sha256:{'b' * 64}",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user