From 7b919dde54e8fa2d00a078aa7126879975c8dd71 Mon Sep 17 00:00:00 2001 From: Shuxin Lin Date: Wed, 14 Oct 2026 22:24:57 -0400 Subject: [PATCH 1/2] fix(tsfm): allowlist the transformers and tabular estimators recipes use The estimator allowlist had no sktime transformers and only one tabular estimator, so the advertised recipe features always failed with "... is not an allowed estimator class": - forecasting `transforms` (e.g. ExponentTransformer, Detrender) - tabular `sktime_class` transforms (e.g. Catch22, MiniRocket) - `tsfm_regression` (no regressor was allowed) - tabular classification/clustering other than RandomForestClassifier Add the series/panel transformers, the trend forecasters and sklearn scalers they nest, and common sklearn regressors, classifiers and KMeans. None take a callable, file path or pickle. Tests cover a transformed forecast, a Catch22 -> RandomForestRegressor regression, and that every allowlisted path imports. Signed-off-by: Shuxin Lin --- src/servers/tsfm/substrate/resolver.py | 34 ++++++++++++++++ src/servers/tsfm/tests/test_no_code_exec.py | 43 +++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/servers/tsfm/substrate/resolver.py b/src/servers/tsfm/substrate/resolver.py index 9797913e..005a1df8 100644 --- a/src/servers/tsfm/substrate/resolver.py +++ b/src/servers/tsfm/substrate/resolver.py @@ -61,6 +61,40 @@ "sktime.clustering.dbscan.TimeSeriesDBSCAN", "sktime.clustering.kernel_k_means.TimeSeriesKernelKMeans", "sktime.clustering.agglomerative.TimeSeriesAgglomerativeClustering", + # recipe `transforms` on a forecast target, and the estimators they nest + "sktime.transformations.series.exponent.ExponentTransformer", + "sktime.transformations.series.exponent.SqrtTransformer", + "sktime.transformations.series.boxcox.BoxCoxTransformer", + "sktime.transformations.series.boxcox.LogTransformer", + "sktime.transformations.series.detrend.Detrender", + "sktime.transformations.series.detrend.Deseasonalizer", + "sktime.transformations.series.detrend.ConditionalDeseasonalizer", + "sktime.transformations.series.difference.Differencer", + "sktime.transformations.series.impute.Imputer", + "sktime.transformations.series.outlier_detection.HampelFilter", + "sktime.transformations.series.adapt.TabularToSeriesAdaptor", + "sktime.forecasting.trend.PolynomialTrendForecaster", + "sktime.forecasting.trend.TrendForecaster", + "sklearn.preprocessing.StandardScaler", + "sklearn.preprocessing.MinMaxScaler", + "sklearn.preprocessing.RobustScaler", + # run_tabular_recipe: panel feature transforms, then the tabular estimator + "sktime.transformations.panel.catch22.Catch22", + "sktime.transformations.panel.rocket.Rocket", + "sktime.transformations.panel.rocket.MiniRocket", + "sktime.transformations.panel.rocket.MiniRocketMultivariate", + "sklearn.ensemble.RandomForestRegressor", + "sklearn.ensemble.ExtraTreesRegressor", + "sklearn.ensemble.GradientBoostingRegressor", + "sklearn.linear_model.LinearRegression", + "sklearn.linear_model.Ridge", + "sklearn.linear_model.Lasso", + "sklearn.neighbors.KNeighborsRegressor", + "sklearn.ensemble.ExtraTreesClassifier", + "sklearn.ensemble.GradientBoostingClassifier", + "sklearn.linear_model.LogisticRegression", + "sklearn.neighbors.KNeighborsClassifier", + "sklearn.cluster.KMeans", }) diff --git a/src/servers/tsfm/tests/test_no_code_exec.py b/src/servers/tsfm/tests/test_no_code_exec.py index e6490cc6..0dbe0d03 100644 --- a/src/servers/tsfm/tests/test_no_code_exec.py +++ b/src/servers/tsfm/tests/test_no_code_exec.py @@ -109,6 +109,49 @@ def test_allowlisted_estimator_still_runs(): assert r.get("status") == "success", r +def test_every_allowed_class_imports(): + from ..substrate.resolver import ALLOWED_CLASSES, _import_target + + for path in ALLOWED_CLASSES: + assert isinstance(_import_target(path), type), path + + +def test_forecast_transforms_still_run(): + r = _forecast({ + "estimator": {"sktime_class": "sktime.forecasting.naive.NaiveForecaster", + "params": {"strategy": "drift"}}, + "transforms": [ + {"sktime_class": "sktime.transformations.series.adapt.TabularToSeriesAdaptor", + "params": {"transformer": {"_target_": "sklearn.preprocessing.StandardScaler"}}}, + {"sktime_class": "sktime.transformations.series.exponent.ExponentTransformer", + "params": {"power": 0.5}}, + {"sktime_class": "sktime.transformations.series.detrend.Detrender", + "params": {"forecaster": { + "_target_": "sktime.forecasting.trend.PolynomialTrendForecaster", + "params": {"degree": 1}}}}, + ], + "fh": [1, 2, 3]}) + assert r.get("status") == "success", r + + +def test_tabular_transform_and_regression_still_run(): + import pandas as pd + + rng = np.random.RandomState(0) + X = rng.randn(30, 24) + np.repeat(np.arange(3), 10)[:, None] + df = pd.DataFrame(X, columns=[f"f{i}" for i in range(24)]) + df["rul"] = np.repeat([30.0, 20.0, 10.0], 10) + rng.rand(30) + path = os.path.join(refs._ensure_workdir(), "noexec_rul.csv") + df.to_csv(path, index=False) + r = _call("run_tabular_recipe", {"dataset_path": path, "label_column": "rul", "recipe": { + "task": "tsfm_regression", + "transforms": [{"sktime_class": "sktime.transformations.panel.catch22.Catch22"}], + "estimator": {"sktime_class": "sklearn.ensemble.RandomForestRegressor", + "params": {"n_estimators": 10}}}}) + assert r.get("status") == "success", r + assert r["metric"] == "r2" and isinstance(r["cv_score"], float), r + + # File pointers: the read-back channel ------------------------------------------------------ def test_file_pointer_outside_data_roots_is_refused(): for p in ("/proc/self/environ", "file:///etc/passwd", "/etc/hostname", From 6a26709d7fb3a1ae170f80a75b9300e37638038d Mon Sep 17 00:00:00 2001 From: Shuxin Lin Date: Wed, 14 Oct 2026 22:30:47 -0400 Subject: [PATCH 2/2] test(tsfm): drop transform/regression tests now covered upstream 08e817b adds test_forecasting_transforms_still_run and test_tabular_regression_still_runs; keep only the allowlist import check. Signed-off-by: Shuxin Lin --- src/servers/tsfm/tests/test_no_code_exec.py | 36 --------------------- 1 file changed, 36 deletions(-) diff --git a/src/servers/tsfm/tests/test_no_code_exec.py b/src/servers/tsfm/tests/test_no_code_exec.py index 01fabd43..7f9b9eaf 100644 --- a/src/servers/tsfm/tests/test_no_code_exec.py +++ b/src/servers/tsfm/tests/test_no_code_exec.py @@ -116,42 +116,6 @@ def test_every_allowed_class_imports(): assert isinstance(_import_target(path), type), path -def test_forecast_transforms_still_run(): - r = _forecast({ - "estimator": {"sktime_class": "sktime.forecasting.naive.NaiveForecaster", - "params": {"strategy": "drift"}}, - "transforms": [ - {"sktime_class": "sktime.transformations.series.adapt.TabularToSeriesAdaptor", - "params": {"transformer": {"_target_": "sklearn.preprocessing.StandardScaler"}}}, - {"sktime_class": "sktime.transformations.series.exponent.ExponentTransformer", - "params": {"power": 0.5}}, - {"sktime_class": "sktime.transformations.series.detrend.Detrender", - "params": {"forecaster": { - "_target_": "sktime.forecasting.trend.PolynomialTrendForecaster", - "params": {"degree": 1}}}}, - ], - "fh": [1, 2, 3]}) - assert r.get("status") == "success", r - - -def test_tabular_transform_and_regression_still_run(): - import pandas as pd - - rng = np.random.RandomState(0) - X = rng.randn(30, 24) + np.repeat(np.arange(3), 10)[:, None] - df = pd.DataFrame(X, columns=[f"f{i}" for i in range(24)]) - df["rul"] = np.repeat([30.0, 20.0, 10.0], 10) + rng.rand(30) - path = os.path.join(refs._ensure_workdir(), "noexec_rul.csv") - df.to_csv(path, index=False) - r = _call("run_tabular_recipe", {"dataset_path": path, "label_column": "rul", "recipe": { - "task": "tsfm_regression", - "transforms": [{"sktime_class": "sktime.transformations.panel.catch22.Catch22"}], - "estimator": {"sktime_class": "sklearn.ensemble.RandomForestRegressor", - "params": {"n_estimators": 10}}}}) - assert r.get("status") == "success", r - assert r["metric"] == "r2" and isinstance(r["cv_score"], float), r - - # File pointers: the read-back channel ------------------------------------------------------ def test_file_pointer_outside_data_roots_is_refused(): for p in ("/proc/self/environ", "file:///etc/passwd", "/etc/hostname",