open-data-sci 0.1.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.
Files changed (85) hide show
  1. open_data_sci-0.1.0.dist-info/METADATA +629 -0
  2. open_data_sci-0.1.0.dist-info/RECORD +85 -0
  3. open_data_sci-0.1.0.dist-info/WHEEL +4 -0
  4. open_data_sci-0.1.0.dist-info/entry_points.txt +2 -0
  5. open_data_sci-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. opendatasci/__init__.py +47 -0
  7. opendatasci/_tui/__init__.py +1 -0
  8. opendatasci/_tui/adapter.py +102 -0
  9. opendatasci/_tui/app.py +429 -0
  10. opendatasci/_tui/commands.py +95 -0
  11. opendatasci/_tui/completion.py +139 -0
  12. opendatasci/_tui/controller.py +644 -0
  13. opendatasci/_tui/file_refs.py +153 -0
  14. opendatasci/_tui/models.py +4 -0
  15. opendatasci/_tui/presenter.py +259 -0
  16. opendatasci/_tui/service.py +78 -0
  17. opendatasci/_tui/session.py +53 -0
  18. opendatasci/_tui/styles.tcss +248 -0
  19. opendatasci/_tui/styles_visible.tcss +245 -0
  20. opendatasci/_tui/theme.py +113 -0
  21. opendatasci/_tui/tools_display.py +86 -0
  22. opendatasci/_tui/widgets.py +1001 -0
  23. opendatasci/_utils/__init__.py +0 -0
  24. opendatasci/_utils/async_utils.py +11 -0
  25. opendatasci/_utils/data_formats.py +135 -0
  26. opendatasci/_utils/hash_utils.py +52 -0
  27. opendatasci/_utils/langchain_utils.py +155 -0
  28. opendatasci/_utils/streaming_utils.py +23 -0
  29. opendatasci/agents/__init__.py +12 -0
  30. opendatasci/agents/agents.py +515 -0
  31. opendatasci/agents/agents_factory.py +71 -0
  32. opendatasci/agents/chat_memory.py +397 -0
  33. opendatasci/agents/graphs.py +84 -0
  34. opendatasci/agents/nodes.py +74 -0
  35. opendatasci/agents/states.py +36 -0
  36. opendatasci/agents/turn_memory.py +124 -0
  37. opendatasci/configs.py +275 -0
  38. opendatasci/context/__init__.py +7 -0
  39. opendatasci/context/base.py +56 -0
  40. opendatasci/context/local.py +236 -0
  41. opendatasci/models/__init__.py +7 -0
  42. opendatasci/models/anthropic.py +40 -0
  43. opendatasci/models/aws.py +86 -0
  44. opendatasci/models/factory.py +179 -0
  45. opendatasci/models/google.py +79 -0
  46. opendatasci/models/local.py +79 -0
  47. opendatasci/models/microsoft.py +62 -0
  48. opendatasci/models/openai.py +49 -0
  49. opendatasci/models/providers.py +12 -0
  50. opendatasci/prompts/__init__.py +5 -0
  51. opendatasci/prompts/builders.py +85 -0
  52. opendatasci/prompts/caching.py +42 -0
  53. opendatasci/prompts/message_templates.py +7 -0
  54. opendatasci/prompts/prompt_templates.py +227 -0
  55. opendatasci/resources/skills/competitive_data_science.md +241 -0
  56. opendatasci/resources/skills/data_science.md +55 -0
  57. opendatasci/resources/skills/data_science_education.md +42 -0
  58. opendatasci/resources/skills/deep_learning.md +205 -0
  59. opendatasci/resources/skills/machine_learning.md +68 -0
  60. opendatasci/resources/skills/quantitative_analysis.md +45 -0
  61. opendatasci/sandbox/__init__.py +14 -0
  62. opendatasci/sandbox/_runner.py +114 -0
  63. opendatasci/sandbox/base.py +170 -0
  64. opendatasci/sandbox/srt.py +490 -0
  65. opendatasci/skills/__init__.py +9 -0
  66. opendatasci/skills/base.py +28 -0
  67. opendatasci/skills/local.py +131 -0
  68. opendatasci/streaming/__init__.py +37 -0
  69. opendatasci/streaming/events.py +159 -0
  70. opendatasci/streaming/processors.py +387 -0
  71. opendatasci/tools/__init__.py +58 -0
  72. opendatasci/tools/coding.py +261 -0
  73. opendatasci/tools/critic.py +136 -0
  74. opendatasci/tools/dataset_info.py +391 -0
  75. opendatasci/tools/factory.py +172 -0
  76. opendatasci/tools/mcp.py +179 -0
  77. opendatasci/tools/planning.py +88 -0
  78. opendatasci/tools/skills.py +90 -0
  79. opendatasci/tools/user_interaction.py +54 -0
  80. opendatasci/tools/web.py +236 -0
  81. opendatasci/tools/workers.py +237 -0
  82. opendatasci/tools/workspace.py +55 -0
  83. opendatasci/workspace/__init__.py +9 -0
  84. opendatasci/workspace/base.py +20 -0
  85. opendatasci/workspace/local.py +25 -0
@@ -0,0 +1,68 @@
1
+ # Machine Learning Skill
2
+
3
+ **Problem Framing**
4
+ - Before writing any code, the prediction task is worth thinking through carefully: what exactly is being predicted, at what point in time, using what information, and with what tolerance for different kinds of errors
5
+ - Label quality often determines the ceiling on model performance more than architecture does — severe imbalance, label noise, and ambiguous labelling criteria are worth surfacing early
6
+ - The data modality (tabular, text, image, time series, graph) and available volume should inform the range of approaches worth considering
7
+ - Leakage — features computed using information that wouldn't be available at prediction time — is the most common source of over-optimistic evaluation results; it's worth tracing feature provenance carefully before trusting any metrics
8
+
9
+ **Splitting Strategy**
10
+ - The split strategy encodes assumptions about the real-world prediction setting; getting it wrong produces metrics that don't generalise
11
+ - Temporal data requires time-ordered splits to avoid the model seeing the future during training — random shuffling of chronological data is a quiet but serious source of leakage
12
+ - When rows share an entity (user, customer, session, location), entity-aware splits prevent the model from memorising entities it will never see again
13
+ - Stratified splits preserve class distribution across folds and are particularly important when classes are rare
14
+ - The test set should be treated as a one-time evaluation; tuning against it invalidates it as an unbiased estimate
15
+
16
+ **Feature Engineering & Selection**
17
+ - Preprocessing transformations need to be fit only on training data and applied consistently to every split; fitting on the full dataset before splitting leaks distributional information into evaluation
18
+ - For tabular data, useful transformations include datetime decomposition, lag and rolling features, interaction terms, and outlier treatment — the right choices are domain-dependent
19
+ - High-cardinality categoricals benefit from encoding strategies that don't naively explode dimensionality; the right approach depends on the model family and available data volume
20
+ - Feature magnitude matters for some model families and is irrelevant for others — scaling decisions should match the model's sensitivity
21
+ - More features is not always better: irrelevant or redundant features add noise and variance, can hurt distance-based models, and make models harder to interpret and debug; feature selection (variance-based filtering, correlation-based pruning, importance-based selection) is often worth doing before fitting complex models
22
+ - Training/inference skew — features computed differently at training time versus inference time — is a common source of silent degradation
23
+
24
+ **Model Selection & Complexity**
25
+ - A minimal baseline establishes the floor: what performance is achievable with no model at all, or with a trivial rule? This makes the value of subsequent complexity concrete and measurable
26
+ - Increasing model complexity should be driven by measured improvement on held-out data, not by a prior assumption that a more powerful model will help — underfitting is a real failure mode too
27
+ - Model selection should follow the problem structure and data characteristics, not a habitual preference for any particular algorithm family — the choice of model is itself a design decision that deserves the same rigour as any other modelling choice
28
+ - For tabular data, a principled progression starts with linear or logistic regression as an interpretable, regularisable baseline; tree-based ensembles (random forests, gradient-boosting variants such as LightGBM, XGBoost, CatBoost, or scikit-learn's HistGradientBoosting) are the natural next step when non-linearity and feature interactions are evident and data volume supports them; neural architectures (MLP, TabNet, FT-Transformer) make sense when data volume is large and the iteration budget supports their tuning overhead — moving up the complexity ladder only when simpler models demonstrably fall short
29
+ - For text and NLP tasks, TF-IDF or bag-of-words features with a linear classifier is a strong, cheap baseline; pre-trained transformer models (BERT-family, smaller distilled variants, domain-specific checkpoints) substantially raise the performance ceiling but carry far higher compute costs — calibrate the choice to the task scale and available resources
30
+ - For time-series and sequential data, statistical methods (ARIMA, ETS, Theta, Prophet) are often sufficient and interpretable when the series is short, labelled features are scarce, or seasonality and trend dominate; ML approaches (tree ensembles with lag features, LSTM, Temporal Fusion Transformer) add value when exogenous signals, many parallel series, or complex non-linear dependencies are present
31
+ - For image and spatial data, pre-trained convolutional or vision-transformer backbones via transfer learning are the standard entry point due to transfer efficiency; simpler feature-based approaches (HOG, colour statistics, patch descriptors) remain viable when labels are very scarce or compute is tightly constrained
32
+ - Model families make different assumptions: linear models assume additive, globally linear effects and are easy to interpret, debug, and regularise; tree-based ensembles capture interactions and non-linearities without explicit feature engineering and are relatively robust to irrelevant features and scale differences; neural networks learn representations end-to-end but require substantially more data, hyperparameter effort, and iteration cycles to realise their potential — these tradeoffs should be reasoned through explicitly for each problem
33
+ - The right level of complexity depends on data volume, feature richness, signal-to-noise ratio, and interpretability requirements; many real problems are solved well by simple models, and complexity beyond what the signal supports hurts rather than helps
34
+ - Ensemble and stacking strategies can narrow the gap between model families and are most effective when combining architecturally distinct model families (e.g., gradient boosting + neural network + linear model) or models trained on different feature sets or data samples — ensembling similar frameworks (e.g., XGBoost, LightGBM, CatBoost) produces highly correlated predictions and therefore little diversity benefit
35
+
36
+ **Hyperparameter Tuning**
37
+ - The search strategy should match the budget: exhaustive search is only feasible for small spaces; random search and Bayesian optimisation cover large spaces more efficiently
38
+ - The wall-clock cost of a search is the product of configurations evaluated and the cost of each evaluation; on large datasets both factors deserve explicit management — run the search on a carefully constructed representative subsample (stratified by class and any structural variable such as time period or entity) to cut per-evaluation time dramatically while preserving most of the directional signal; techniques like successive halving and Hyperband go further by pruning unpromising configurations early rather than running every trial to completion; always re-train or re-evaluate the winning configuration on the full dataset before committing to it
39
+ - The same cross-validation strategy used for model evaluation should be used during tuning to keep estimates consistent
40
+ - Reporting the distribution of CV scores across configurations, not just the best, gives a clearer picture of sensitivity to hyperparameter choices
41
+ - All stochastic components should have fixed random seeds to ensure results are stable across runs
42
+
43
+ **Overfitting & Regularisation**
44
+ - The gap between training and validation performance is the primary diagnostic for overfitting; a model that performs well on training data but poorly on held-out data has memorised rather than generalised
45
+ - Overfitting risk increases with model capacity relative to data volume — more parameters, more trees, deeper networks all have higher capacity and require more data or more regularisation to generalise
46
+ - Regularisation techniques (penalising model complexity, limiting depth, adding noise during training, early stopping) are the primary tools for closing the train/val gap; the right form depends on the model family
47
+ - Learning curves (performance as a function of training set size) are a useful diagnostic: poor performance that improves with more data suggests a data problem; a persistent train/val gap that doesn't close suggests a regularisation or capacity problem
48
+ - Underfitting — where even training performance is poor — points in the opposite direction: the model may lack the capacity or the features to capture the signal
49
+
50
+ **Evaluation & Diagnostics**
51
+ - Aggregate metrics can hide a lot; slicing by relevant subgroups, prediction ranges, or time periods often reveals where a model underperforms in ways the headline number conceals
52
+ - Error analysis — directly examining mispredictions (false positives, false negatives, worst residuals, confused classes) — is often more informative than metrics alone; patterns in where a model fails point directly at what to fix
53
+ - For classification, the decision threshold affects the precision-recall tradeoff and should be chosen deliberately based on the relative cost of false positives versus false negatives, not left at a default
54
+ - Probability calibration matters when predicted scores are used as actual probability estimates rather than just rankings
55
+ - Residual analysis for regression surfaces systematic patterns — heteroscedasticity, non-linearity, outlier influence — that aggregate error metrics don't capture
56
+ - Cross-validation spread (mean ± std across folds) characterises how stable performance is, not just what the best-case number is
57
+
58
+ **Class Imbalance**
59
+ - Class imbalance affects both what the model learns and how performance is measured; addressing only one side gives a misleading picture
60
+ - Threshold adjustment and class weighting are low-cost interventions that often recover substantial minority-class performance without resampling
61
+ - Resampling techniques change the training distribution and should only ever be applied within the training fold — contaminating validation or test data with synthetic samples invalidates evaluation
62
+ - Extreme imbalance changes the problem framing: precision at a given recall threshold or anomaly detection approaches may be more appropriate than standard classification evaluation
63
+
64
+ **Interpretability**
65
+ - Understanding what a model has learned is distinct from understanding why it performs well; feature importance methods address the former
66
+ - Global importances summarise the model's overall behaviour across the dataset; local explanations explain individual predictions — both are useful but answer different questions
67
+ - Impurity-based importance measures are known to favour high-cardinality features and can be misleading; permutation-based importance and gradient-based attribution methods are generally more reliable
68
+ - Interpretability requirements should inform model choice early; post-hoc approximations of black-box models have their own failure modes
@@ -0,0 +1,45 @@
1
+ # Quantitative Analysis Skill
2
+
3
+ **Problem Formulation**
4
+ - Translating a real-world question into a precise mathematical statement is the first and often most consequential step; ambiguity in the objective propagates into every downstream modelling choice
5
+ - Distinguishing between estimation problems (what is the value of some unknown quantity?), prediction problems (what will happen?), and optimisation problems (what should we do?) shapes the entire analytical approach
6
+ - Constraints and feasibility requirements are as important as the objective; an optimal solution that violates real-world constraints is not a solution
7
+
8
+ **Mathematical & Statistical Foundations**
9
+ - Results derived from first principles are more defensible than results produced by black-box procedures; being able to trace a conclusion back to its assumptions is essential for knowing when to trust it
10
+ - Linearity assumptions are convenient but often wrong; understanding where a linear approximation holds and where it breaks down is a core skill
11
+ - Probability distributions carry specific assumptions about the data-generating process; selecting a distribution because it fits historical data is not the same as selecting one because it reflects the underlying mechanism
12
+ - Stationarity assumptions underlie much of classical time series analysis; checking for non-stationarity (unit roots, structural breaks, regime changes) before applying methods that assume it prevents misleading inference
13
+ - Heavy tails and extreme events are often the quantities that matter most in risk-sensitive applications; thin-tail distributional assumptions systematically underestimate tail risk
14
+
15
+ **Time Series & Signal Analysis**
16
+ - Autocorrelation structure (ACF/PACF plots, Ljung-Box tests) should inform model choice before fitting; ignoring it leads to invalid standard errors and spurious relationships
17
+ - Spurious regression between integrated time series is a well-documented failure mode; cointegration analysis is the appropriate tool for modelling long-run relationships between non-stationary series
18
+ - Seasonality, trend, and irregular components are often better handled explicitly than absorbed into a single model; decomposition clarifies what each component contributes
19
+ - Volatility clustering — the empirical regularity that large moves tend to follow large moves — is a persistent feature of financial and economic time series that standard models ignore; GARCH-family models are the standard treatment
20
+ - Choosing the forecast horizon deliberately matters: the right model for one-step-ahead forecasting is often not the right model for long-horizon forecasting
21
+
22
+ **Risk & Uncertainty Quantification**
23
+ - Point estimates without uncertainty bounds are incomplete; the width of the confidence or credible interval is often more decision-relevant than the point itself
24
+ - Scenario analysis and stress testing complement statistical risk measures by exploring tail outcomes that may not be well-represented in historical data
25
+ - Model risk — the risk that the model itself is wrong — is a distinct and often underappreciated source of uncertainty; comparing results across plausible alternative models is a useful guard
26
+ - Tail risk measures (VaR, CVaR/Expected Shortfall) answer different questions: VaR describes a threshold, CVaR describes what to expect when that threshold is breached — for many purposes CVaR is the more informative measure
27
+ - Monte Carlo simulation provides a flexible framework for propagating uncertainty through complex models; the quality of the output depends entirely on the quality of the input distribution assumptions
28
+
29
+ **Optimisation**
30
+ - Many quantitative problems can be cast as optimisation; recognising the structure (convex vs. non-convex, constrained vs. unconstrained, continuous vs. integer) determines what solvers are applicable and what guarantees are available
31
+ - Convex problems have the substantial advantage that local optima are global optima; non-convex problems may require heuristics, multiple starting points, or relaxations
32
+ - Numerical stability matters: poorly conditioned problems can produce results that look precise but are sensitive to small perturbations in inputs or solver tolerances
33
+ - In practice, regularisation in optimisation and regularisation in statistics are the same idea expressed in different languages — both bias a solution toward simpler structure in exchange for reduced variance
34
+
35
+ **Backtesting & Empirical Validation**
36
+ - Backtesting on historical data is a necessary but insufficient validation; without controls for look-ahead bias, survivorship bias, and overfitting, backtest results are unreliable guides to out-of-sample performance
37
+ - Walk-forward validation — fitting on a rolling training window and evaluating on a subsequent out-of-sample period — better mimics the real operational setting than a single historical simulation
38
+ - Multiple testing inflates apparent strategy performance; the more configurations that are tried on the same historical period, the more likely the best-performing one succeeds by chance
39
+ - Transaction costs, slippage, and capacity constraints routinely close the gap between theoretical and realised performance; a backtest that ignores them is optimistic by construction
40
+
41
+ **Communicating Quantitative Results**
42
+ - Numerical precision in outputs should match the precision of the inputs; reporting eight decimal places from a model estimated on noisy data implies false certainty
43
+ - Assumptions deserve explicit documentation: a result is only as valid as the assumptions that support it, and readers need to be able to evaluate those assumptions for themselves
44
+ - Sensitivity analysis — showing how conclusions change as key inputs or assumptions vary — is often more valuable than a single point result, particularly when inputs are uncertain or contested
45
+ - The practical significance of a quantitative result (does the difference matter for the decision at hand?) is distinct from its statistical significance and should always be addressed
@@ -0,0 +1,14 @@
1
+ from opendatasci.sandbox.base import (
2
+ BaseSandbox,
3
+ BaseSandboxFactory,
4
+ SandboxExecResult,
5
+ )
6
+ from opendatasci.sandbox.srt import SRTSandbox, SRTSandboxFactory
7
+
8
+ __all__ = [
9
+ "BaseSandboxFactory",
10
+ "SandboxExecResult",
11
+ "BaseSandbox",
12
+ "SRTSandbox",
13
+ "SRTSandboxFactory",
14
+ ]
@@ -0,0 +1,114 @@
1
+ """Injected into the SRT sandbox session directory and executed as a subprocess.
2
+
3
+ Reads user code from an env-var, executes it inside a persistent namespace,
4
+ and emits a single JSON payload to stdout.
5
+ """
6
+
7
+ import base64
8
+ import io
9
+ import json
10
+ import os
11
+ import pickle
12
+ import sys
13
+ import traceback
14
+ from pathlib import Path
15
+
16
+ try:
17
+ import pandas as pd
18
+ except ImportError:
19
+ pd = None # type: ignore[assignment]
20
+
21
+ STATE_PATH = os.environ.get("OPENDATASCI_STATE_PATH", "/tmp/opendatasci_state.pkl")
22
+ WORKSPACE = os.environ.get("OPENDATASCI_WORKSPACE", "/tmp/opendatasci_workspace")
23
+ RESULTS_KEY = "__opendatasci_results__"
24
+ code = base64.b64decode(os.environ["OPENDATASCI_CODE_B64"]).decode("utf-8")
25
+
26
+ # Trust note: state.pkl is a per-session, sandbox-private temp file written only
27
+ # by this runner, so unpickling it is safe today. If state ever becomes shared
28
+ # or persisted across sessions (e.g. a microservice port), this load() becomes
29
+ # an arbitrary-code-execution vector and must be replaced with a safe format.
30
+ try:
31
+ with open(STATE_PATH, "rb") as fh:
32
+ namespace = pickle.load(fh)
33
+ except FileNotFoundError:
34
+ namespace = {}
35
+
36
+ workspacedir = Path(WORKSPACE)
37
+ opendatasci_directory = workspacedir / ".opendatasci"
38
+ opendatasci_directory.mkdir(parents=True, exist_ok=True)
39
+ os.chdir(str(workspacedir))
40
+
41
+ saved_results = namespace.pop(RESULTS_KEY, {})
42
+
43
+
44
+ def save_result(name: str, value: object) -> None:
45
+ saved_results[name] = value
46
+
47
+
48
+ namespace.update(
49
+ {
50
+ "workspacedir": workspacedir,
51
+ "opendatasci_directory": opendatasci_directory,
52
+ "save_result": save_result,
53
+ }
54
+ )
55
+
56
+ skip_keys = {"workspacedir", "opendatasci_directory", "save_result", "__builtins__"}
57
+ captured = io.StringIO()
58
+
59
+ original_stdout = sys.stdout
60
+ sys.stdout = captured
61
+ sys.stdin = io.StringIO("")
62
+ try:
63
+ exec(compile(code, "<opendatasci>", "exec"), namespace) # noqa: S102
64
+ output_value = namespace.pop("result", None)
65
+
66
+ clean_ns = {}
67
+ var_info = {}
68
+ dropped = []
69
+
70
+ for key, value in namespace.items():
71
+ if key.startswith("_") or key in skip_keys:
72
+ continue
73
+
74
+ if pd is not None and isinstance(value, pd.DataFrame):
75
+ description = f"DataFrame {value.shape}"
76
+ elif isinstance(value, (list, dict)):
77
+ description = f"{type(value).__name__} (len={len(value)})"
78
+ else:
79
+ description = type(value).__name__
80
+
81
+ try:
82
+ pickle.dumps(value)
83
+ except Exception:
84
+ dropped.append(key)
85
+ continue
86
+
87
+ clean_ns[key] = value
88
+ var_info[key] = description
89
+
90
+ clean_ns[RESULTS_KEY] = saved_results
91
+ with open(STATE_PATH, "wb") as fh_out:
92
+ pickle.dump(clean_ns, fh_out)
93
+
94
+ payload = {
95
+ "success": True,
96
+ "stdout": captured.getvalue(),
97
+ "result": repr(output_value) if output_value is not None else None,
98
+ "var_info": var_info,
99
+ "saved_results": {k: repr(v) for k, v in saved_results.items()},
100
+ "dropped_vars": dropped,
101
+ }
102
+ except Exception as exc:
103
+ payload = {
104
+ "success": False,
105
+ "stdout": captured.getvalue(),
106
+ "error": f"{type(exc).__name__}: {exc}\n{traceback.format_exc()}",
107
+ "var_info": {},
108
+ "saved_results": {},
109
+ "dropped_vars": [],
110
+ }
111
+ finally:
112
+ sys.stdout = original_stdout
113
+
114
+ print(json.dumps(payload))
@@ -0,0 +1,170 @@
1
+ """Abstract sandbox interface for Python and TUI code execution."""
2
+
3
+ import re
4
+ import shlex
5
+ from abc import ABC, abstractmethod
6
+ from contextlib import AbstractAsyncContextManager
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ @dataclass
13
+ class SandboxExecResult:
14
+ """Result of a single Python or TUI execution in the sandbox."""
15
+
16
+ success: bool
17
+ output: Any = None
18
+ stdout: str = ""
19
+ error: str | None = None
20
+ code: str = ""
21
+
22
+
23
+ ALLOWED_CLI_COMMANDS: frozenset[str] = frozenset(
24
+ {
25
+ # Directory listing & navigation
26
+ "ls",
27
+ "dir",
28
+ "pwd",
29
+ "tree",
30
+ # File viewing
31
+ "cat",
32
+ "head",
33
+ "tail",
34
+ "file",
35
+ "stat",
36
+ # Text search & discovery
37
+ "grep",
38
+ "find",
39
+ "which",
40
+ # Text processing (read-oriented)
41
+ "cut",
42
+ "sort",
43
+ "uniq",
44
+ "wc",
45
+ "awk",
46
+ "sed",
47
+ "tr",
48
+ "strings",
49
+ # File comparison & checksums
50
+ "diff",
51
+ "cmp",
52
+ "md5sum",
53
+ "sha256sum",
54
+ "shasum",
55
+ # Structured data / binary inspection
56
+ "jq",
57
+ "xxd",
58
+ "od",
59
+ # General output & info
60
+ "echo",
61
+ "printf",
62
+ "date",
63
+ "uname",
64
+ "printenv",
65
+ "env",
66
+ # Archive inspection (listing only)
67
+ "unzip",
68
+ "tar",
69
+ "zip",
70
+ }
71
+ )
72
+
73
+ _FORBIDDEN_CLI_OPERATORS: tuple[str, ...] = ("||", ";", "`", "$(", ">", "<")
74
+ _ALLOWED_CLI_SHELL_OPERATORS: frozenset[str] = frozenset({"|", "&&"})
75
+
76
+
77
+ def validate_cli_command(command: str) -> str | None:
78
+ """Validate *command* against the TUI allowlist.
79
+
80
+ Returns an error string describing the first violation found, or ``None``
81
+ if the command is valid. This function is pure (no side effects) and can
82
+ be called from tests directly.
83
+ """
84
+ command = command.strip()
85
+ if not command:
86
+ return "Empty command."
87
+
88
+ for op in _FORBIDDEN_CLI_OPERATORS:
89
+ if op in command:
90
+ return f"Shell operator '{op}' is not allowed."
91
+
92
+ use_shell = any(op in command for op in _ALLOWED_CLI_SHELL_OPERATORS)
93
+ segments = re.split(r"&&|\|(?!\|)", command) if use_shell else [command]
94
+
95
+ for seg in segments:
96
+ seg = seg.strip()
97
+ if not seg:
98
+ return "Empty pipeline segment."
99
+ try:
100
+ parts = shlex.split(seg)
101
+ except ValueError as exc:
102
+ return f"Invalid command syntax: {exc}"
103
+ cmd_name = Path(parts[0]).name.lower()
104
+ if cmd_name not in ALLOWED_CLI_COMMANDS:
105
+ sample = sorted(ALLOWED_CLI_COMMANDS)
106
+ return (
107
+ f"Command '{cmd_name}' is not allowed. "
108
+ f"Permitted commands include: {', '.join(sample)}, … "
109
+ f"({len(ALLOWED_CLI_COMMANDS)} total)"
110
+ )
111
+
112
+ return None
113
+
114
+
115
+ class BaseSandbox(ABC):
116
+ """Stateful code execution sandbox scoped to a single agent session.
117
+
118
+ Responsible for running Python code and TUI commands, capturing output,
119
+ and preserving state (variables, results) across turns within the same
120
+ conversation.
121
+ """
122
+
123
+ @abstractmethod
124
+ async def execute(self, code: str) -> SandboxExecResult:
125
+ """Execute Python *code* and return the result."""
126
+
127
+ @abstractmethod
128
+ async def execute_cli(self, command: str) -> SandboxExecResult:
129
+ """Execute a TUI *command* and return the result.
130
+
131
+ Implementations must validate *command* against ``validate_cli_command``
132
+ before running it and return a failed ``SandboxExecResult`` on violation.
133
+ """
134
+
135
+ @abstractmethod
136
+ def reset(self) -> None:
137
+ """Clear all session state (history, variables, results)."""
138
+
139
+ async def close(self) -> None:
140
+ """Release external resources held by this sandbox.
141
+
142
+ The default implementation is a no-op. Override in sandboxes that
143
+ manage external processes or connections (e.g. Docker containers).
144
+ """
145
+
146
+
147
+ class BaseSandboxFactory(ABC):
148
+ """Abstract factory that creates :class:`BaseSandbox` instances via async context managers.
149
+
150
+ Callers must always acquire a sandbox through :meth:`create` so that
151
+ teardown is guaranteed regardless of how the calling code exits::
152
+
153
+ async with factory.create(workspace_path=path) as sandbox:
154
+ result = await sandbox.execute(code)
155
+ # sandbox.close() has been called here
156
+ """
157
+
158
+ @abstractmethod
159
+ def create(
160
+ self, workspace_path: Path | None = None
161
+ ) -> AbstractAsyncContextManager["BaseSandbox"]:
162
+ """Return an async context manager that yields a fresh :class:`BaseSandbox`.
163
+
164
+ The sandbox is closed automatically when the context manager exits,
165
+ whether by normal return, exception, or cancellation.
166
+
167
+ Args:
168
+ workspace_path: Optional filesystem root that the sandbox should
169
+ treat as its working directory.
170
+ """