shellsim 0.1.0__cp39-abi3-win_amd64.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.
shellsim/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """Typed Python interface to shellsim's deterministic execution environment."""
2
+
3
+ from ._api import (
4
+ CommandUsage,
5
+ Environment,
6
+ Invocation,
7
+ Limits,
8
+ MountResult,
9
+ RunResult,
10
+ SimulationError,
11
+ Usage,
12
+ run,
13
+ )
14
+
15
+ __all__ = [
16
+ "CommandUsage",
17
+ "Environment",
18
+ "Invocation",
19
+ "Limits",
20
+ "MountResult",
21
+ "RunResult",
22
+ "SimulationError",
23
+ "Usage",
24
+ "run",
25
+ ]
shellsim/_api.py ADDED
@@ -0,0 +1,300 @@
1
+ """Public, typed facade over shellsim's narrow native adapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import json
7
+ import os
8
+ from typing import Any, Mapping, Optional, Tuple, Union
9
+
10
+ from . import _native
11
+
12
+
13
+ SimulationError = _native.SimulationError
14
+
15
+ _MAX_U64 = (1 << 64) - 1
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Limits:
20
+ """Cumulative resource limits for one simulated environment."""
21
+
22
+ cpu: int = 10_000_000
23
+ memory: int = 64 * 1024 * 1024
24
+ disk: int = 64 * 1024 * 1024
25
+ output: int = 4 * 1024 * 1024
26
+
27
+ def __post_init__(self) -> None:
28
+ for name in ("cpu", "memory", "disk", "output"):
29
+ _validate_limit(name, getattr(self, name))
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Usage:
34
+ """Cumulative resource usage after an action."""
35
+
36
+ cpu_used: int
37
+ memory_current: int
38
+ memory_peak: int
39
+ disk_current: int
40
+ disk_peak: int
41
+ output_bytes: int
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class CommandUsage:
46
+ """Cumulative resource delta recorded for one completed command."""
47
+
48
+ command: str
49
+ cpu: int
50
+ disk_delta: int
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class Invocation:
55
+ """One command occurrence observed during an action."""
56
+
57
+ sequence: int
58
+ pid: int
59
+ argv: Tuple[str, ...]
60
+ trust: str
61
+ status: Optional[int]
62
+ cpu: Optional[int]
63
+ disk_delta: Optional[int]
64
+ unsupported_reason: Optional[str] = None
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class MountResult:
69
+ """Result of copying an explicitly trusted host tree into the VFS."""
70
+
71
+ files: int
72
+ skipped_directories: Tuple[str, ...]
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class RunResult:
77
+ """Byte-preserving output and structured fidelity/resource telemetry for one action."""
78
+
79
+ returncode: int
80
+ stdout: bytes
81
+ stderr: bytes
82
+ stop_reason: Optional[str]
83
+ limits: Limits
84
+ usage: Usage
85
+ command_usage: Tuple[CommandUsage, ...]
86
+ cost_model_version: int
87
+ unsupported: Tuple[str, ...]
88
+ dropped_unsupported: int
89
+ commands: Tuple[str, ...]
90
+ dropped_commands: int
91
+ noop_commands: Tuple[str, ...]
92
+ partial_commands: Tuple[str, ...]
93
+ invocations: Tuple[Invocation, ...]
94
+ dropped_invocations: int
95
+
96
+ @property
97
+ def stdout_text(self) -> str:
98
+ """Decode stdout as UTF-8, replacing malformed sequences."""
99
+
100
+ return self.stdout.decode("utf-8", errors="replace")
101
+
102
+ @property
103
+ def stderr_text(self) -> str:
104
+ """Decode stderr as UTF-8, replacing malformed sequences."""
105
+
106
+ return self.stderr.decode("utf-8", errors="replace")
107
+
108
+ def check_returncode(self) -> None:
109
+ """Raise `SimulationError` when the simulated action did not succeed."""
110
+
111
+ if self.returncode != 0:
112
+ diagnostic = self.stderr_text.strip()
113
+ suffix = f": {diagnostic}" if diagnostic else ""
114
+ raise SimulationError(
115
+ f"shellsim action exited with status {self.returncode}{suffix}"
116
+ )
117
+
118
+
119
+ class Environment:
120
+ """A persistent deterministic machine with an isolated in-memory filesystem.
121
+
122
+ Resource limits and usage are cumulative. CPU, memory, or output exhaustion permanently
123
+ terminates the environment; subsequent calls return the same terminal outcome without work.
124
+ """
125
+
126
+ def __init__(
127
+ self,
128
+ limits: Optional[Limits] = None,
129
+ *,
130
+ cpu: Optional[int] = None,
131
+ memory: Optional[int] = None,
132
+ disk: Optional[int] = None,
133
+ output: Optional[int] = None,
134
+ ) -> None:
135
+ overrides = {"cpu": cpu, "memory": memory, "disk": disk, "output": output}
136
+ if limits is not None and any(value is not None for value in overrides.values()):
137
+ raise ValueError("limits cannot be combined with per-resource overrides")
138
+ if limits is not None and not isinstance(limits, Limits):
139
+ raise TypeError("limits must be a shellsim.Limits instance")
140
+ resolved = limits or Limits(
141
+ **{
142
+ name: _validate_limit(name, value)
143
+ for name, value in overrides.items()
144
+ if value is not None
145
+ }
146
+ )
147
+ self._native = _native.NativeEnvironment(
148
+ resolved.cpu,
149
+ resolved.memory,
150
+ resolved.disk,
151
+ resolved.output,
152
+ )
153
+
154
+ @property
155
+ def terminated(self) -> bool:
156
+ """Whether a terminal resource limit has stopped this environment."""
157
+
158
+ return bool(self._native.terminated)
159
+
160
+ def run(
161
+ self, source: str, stdin: Union[bytes, bytearray, memoryview] = b""
162
+ ) -> RunResult:
163
+ """Execute one complete shell action with an explicit input byte stream."""
164
+
165
+ if not isinstance(source, str):
166
+ raise TypeError("source must be str")
167
+ metadata, stdout, stderr = self._native.run(source, _as_bytes("stdin", stdin))
168
+ return _decode_result(metadata, stdout, stderr)
169
+
170
+ def write_file(
171
+ self,
172
+ path: str,
173
+ data: Union[bytes, bytearray, memoryview, str],
174
+ *,
175
+ mode: int = 0o644,
176
+ ) -> None:
177
+ """Write exact bytes, or UTF-8 text, to one VFS path."""
178
+
179
+ if not isinstance(path, str):
180
+ raise TypeError("path must be str")
181
+ if isinstance(data, str):
182
+ encoded = data.encode()
183
+ else:
184
+ encoded = _as_bytes("data", data)
185
+ if isinstance(mode, bool) or not isinstance(mode, int):
186
+ raise TypeError("mode must be int")
187
+ if not 0 <= mode <= 0o7777:
188
+ raise ValueError("mode must be between 0 and 0o7777")
189
+ self._native.write_file(path, encoded, mode)
190
+
191
+ def read_file(self, path: str) -> bytes:
192
+ """Read one VFS file without decoding its contents."""
193
+
194
+ if not isinstance(path, str):
195
+ raise TypeError("path must be str")
196
+ return self._native.read_file(path)
197
+
198
+ def mkdir(self, path: str, *, parents: bool = False) -> None:
199
+ """Create one VFS directory, optionally including missing parents."""
200
+
201
+ if not isinstance(path, str):
202
+ raise TypeError("path must be str")
203
+ if not isinstance(parents, bool):
204
+ raise TypeError("parents must be bool")
205
+ self._native.mkdir(path, parents)
206
+
207
+ def mount(
208
+ self, host_root: Union[str, os.PathLike[str]], destination: str = "/work"
209
+ ) -> MountResult:
210
+ """Copy an explicitly trusted host directory into the bounded VFS.
211
+
212
+ The walk rejects symlinks and non-regular files, has a 10,000-file limit, and skips
213
+ `.git`, `.venv`, `venv`, `target`, `node_modules`, and `__pycache__` directories. A failed
214
+ import leaves the VFS unchanged. The trusted host tree must not be mutated concurrently.
215
+ """
216
+
217
+ root = os.fspath(host_root)
218
+ if not isinstance(root, str):
219
+ raise TypeError("host_root must resolve to a text path")
220
+ if not isinstance(destination, str):
221
+ raise TypeError("destination must be str")
222
+ report = json.loads(self._native.mount(root, destination))
223
+ return MountResult(
224
+ files=report["files"],
225
+ skipped_directories=tuple(report["skipped_directories"]),
226
+ )
227
+
228
+
229
+ def run(
230
+ source: str,
231
+ stdin: Union[bytes, bytearray, memoryview] = b"",
232
+ limits: Optional[Limits] = None,
233
+ *,
234
+ cpu: Optional[int] = None,
235
+ memory: Optional[int] = None,
236
+ disk: Optional[int] = None,
237
+ output: Optional[int] = None,
238
+ ) -> RunResult:
239
+ """Execute one action in a fresh environment."""
240
+
241
+ return Environment(
242
+ limits,
243
+ cpu=cpu,
244
+ memory=memory,
245
+ disk=disk,
246
+ output=output,
247
+ ).run(source, stdin)
248
+
249
+
250
+ def _validate_limit(name: str, value: Any) -> int:
251
+ if isinstance(value, bool) or not isinstance(value, int):
252
+ raise TypeError(f"{name} must be int")
253
+ if not 0 <= value <= _MAX_U64:
254
+ raise ValueError(f"{name} must be between 0 and {_MAX_U64}")
255
+ return value
256
+
257
+
258
+ def _as_bytes(name: str, value: Any) -> bytes:
259
+ if not isinstance(value, (bytes, bytearray, memoryview)):
260
+ raise TypeError(f"{name} must be bytes-like")
261
+ return bytes(value)
262
+
263
+
264
+ def _decode_result(metadata_json: str, stdout: bytes, stderr: bytes) -> RunResult:
265
+ metadata: Mapping[str, Any] = json.loads(metadata_json)
266
+ outcome = metadata["outcome"]
267
+ limits = Limits(**outcome["limits"])
268
+ usage = Usage(**outcome["usage"])
269
+ command_usage = tuple(CommandUsage(**item) for item in outcome["command_usage"])
270
+ invocations = tuple(
271
+ Invocation(
272
+ sequence=item["sequence"],
273
+ pid=item["pid"],
274
+ argv=tuple(item["argv"]),
275
+ trust=item["trust"],
276
+ status=item["status"],
277
+ cpu=item["cpu"],
278
+ disk_delta=item["disk_delta"],
279
+ unsupported_reason=item.get("unsupported_reason"),
280
+ )
281
+ for item in metadata["invocations"]
282
+ )
283
+ return RunResult(
284
+ returncode=outcome["exit_status"],
285
+ stdout=stdout,
286
+ stderr=stderr,
287
+ stop_reason=outcome["stop_reason"],
288
+ limits=limits,
289
+ usage=usage,
290
+ command_usage=command_usage,
291
+ cost_model_version=outcome["cost_model_version"],
292
+ unsupported=tuple(metadata["unsupported"]),
293
+ dropped_unsupported=metadata["dropped_unsupported"],
294
+ commands=tuple(metadata["commands"]),
295
+ dropped_commands=metadata["dropped_commands"],
296
+ noop_commands=tuple(metadata["noop_commands"]),
297
+ partial_commands=tuple(metadata["partial_commands"]),
298
+ invocations=invocations,
299
+ dropped_invocations=metadata["dropped_invocations"],
300
+ )
shellsim/_native.pyd ADDED
Binary file
shellsim/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,317 @@
1
+ Metadata-Version: 2.4
2
+ Name: shellsim
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: License :: OSI Approved :: Apache Software License
6
+ Classifier: Operating System :: MacOS :: MacOS X
7
+ Classifier: Operating System :: Microsoft :: Windows
8
+ Classifier: Operating System :: POSIX :: Linux
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: Implementation :: CPython
11
+ Classifier: Programming Language :: Rust
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Classifier: Typing :: Typed
14
+ License-File: LICENSE
15
+ Summary: Deterministic, resource-constrained shell and Python execution for tests
16
+ Keywords: sandbox,shell,simulation,testing
17
+ Author: The shellsim authors
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
20
+ Project-URL: Homepage, https://github.com/rjpower/shellsim
21
+ Project-URL: Issues, https://github.com/rjpower/shellsim/issues
22
+ Project-URL: Repository, https://github.com/rjpower/shellsim
23
+
24
+ # shellsim
25
+
26
+ `shellsim` is a deterministic, resource-constrained BusyBox-like environment for evaluating
27
+ agents. Shell programs and Unix-style commands run in-process against an in-memory filesystem;
28
+ they never execute host programs or use the host filesystem as their working environment.
29
+
30
+ The resource model is deliberately approximate. Commands use ordinary Rust data structures while
31
+ reserving modeled memory and charging stable abstract CPU units. This keeps the model predictable,
32
+ cheap, and easy to tune.
33
+
34
+ See [docs/agent-environment.md](docs/agent-environment.md) for the reviewed gap between the current
35
+ simulator and a useful Unix-shaped coding-agent harness, plus the ordered implementation roadmap.
36
+
37
+ ## Resource model
38
+
39
+ - **CPU** is monotonic fuel. Parsing, executor nodes, dispatch, input, output, and algorithms
40
+ consume units. Exhaustion stops the evaluation.
41
+ - **Memory** is modeled concurrent working set. Command reservations are released on return;
42
+ nested invocations contribute to the same peak.
43
+ - **Disk** is logical in-memory filesystem size. Content and a fixed 256-byte non-root node overhead count.
44
+ Mutations that exceed quota roll back atomically, and deletion releases capacity.
45
+ - **Output** caps materialized stdout and stderr as a safety guardrail.
46
+
47
+ Defaults are 10,000,000 CPU units, 64 MiB memory, 64 MiB disk, and 4 MiB output. Costs are
48
+ deterministic rather than cycle-accurate. Results include a cost-model version.
49
+
50
+ ## Build and use
51
+
52
+ ```sh
53
+ cargo build --release
54
+
55
+ # Ordinary output
56
+ ./target/release/shellsim -c 'printf "b\na\n" | sort'
57
+
58
+ # Host file used only as script source; execution occurs in a fresh simulated environment
59
+ ./target/release/shellsim run script.sh arg1 arg2
60
+
61
+ # Persistent interactive session; state and resource usage accumulate until exit/exhaustion
62
+ ./target/release/shellsim shell --cpu 100k --memory 8m --disk 2m --output 64k
63
+
64
+ # Structured evaluation report
65
+ ./target/release/shellsim eval \
66
+ --cpu 100k --memory 8m --disk 2m --output 64k \
67
+ -c 'printf "b\na\n" | sort > result.txt; cat result.txt'
68
+
69
+ # Persistent NDJSON harness session
70
+ printf '%s\n' \
71
+ '{"id":1,"op":"execute","source":"printf hello > result"}' \
72
+ '{"id":2,"op":"workspace_diff"}' \
73
+ | ./target/release/shellsim serve --root ./project
74
+
75
+ # Retain an action, observe its timer wait, then permit virtual-time advancement
76
+ printf '%s\n' \
77
+ '{"id":1,"op":"start_execute","source":"printf one; sleep 2; printf two"}' \
78
+ '{"id":2,"op":"poll_action","action_id":0,"work_quanta":100,"advance_time":false}' \
79
+ '{"id":3,"op":"read_action_output","action_id":0}' \
80
+ '{"id":4,"op":"poll_action","action_id":0,"work_quanta":100,"advance_time":true}' \
81
+ | ./target/release/shellsim serve
82
+
83
+ # Cancel a blocked foreground action while keeping the session reusable
84
+ printf '%s\n' \
85
+ '{"id":1,"op":"start_execute","source":"sleep 60"}' \
86
+ '{"id":2,"op":"cancel_action","action_id":0}' \
87
+ '{"id":3,"op":"execute","source":"printf reused"}' \
88
+ | ./target/release/shellsim serve
89
+
90
+ # Running background jobs can return to the modeled terminal foreground
91
+ ./target/release/shellsim -c 'sleep 2 & fg %1; echo complete'
92
+
93
+ # Fork session zero and route an independent action to the branch
94
+ printf '%s\n' \
95
+ '{"id":1,"op":"fork_session","source":0}' \
96
+ '{"id":2,"session_id":1,"op":"execute","source":"printf branch"}' \
97
+ | ./target/release/shellsim serve
98
+
99
+ # Replay a bounded scenario and emit paired request/response transcript records
100
+ ./target/release/shellsim replay scenario.ndjson --root ./project > transcript.ndjson
101
+
102
+ # Enable format checks, strict trust, and final assertions with a metadata line
103
+ printf '%s\n' '{"scenario":{"version":1,"strict":true,"final_expectation":{"active_action_count":0}}}' \
104
+ '{"op":"execute","source":"make test"}' > strict-scenario.ndjson
105
+ ./target/release/shellsim replay strict-scenario.ndjson
106
+
107
+ # Import a host Python project into a fresh VFS and run it in shellsim
108
+ ./target/release/shellsim-python project/main.py -- arg1
109
+ ./target/release/shellsim-python project/tests --pytest
110
+ ./target/release/shellsim-python --json --root project project/main.py
111
+
112
+ # Embed a persistent simulated environment from Python
113
+ python -m pip install shellsim
114
+ python - <<'PY'
115
+ import shellsim
116
+
117
+ environment = shellsim.Environment(cpu=100_000)
118
+ environment.write_file("/work/main.py", "print(6 * 7)\n")
119
+ result = environment.run("python3.14 /work/main.py")
120
+ assert result.stdout == b"42\n"
121
+ PY
122
+ ```
123
+
124
+ Limit values accept `k`, `m`, and `g` binary suffixes. Arguments after `--` in `eval` mode become
125
+ shell positional parameters.
126
+
127
+ `serve` retains one environment across requests. `--root` performs one trusted, bounded import
128
+ before request processing. The protocol supports shell actions, base64 file reads and writes
129
+ confined to `/work`, stable path-level workspace diffs, checkpoints, VFS reset, listings, and
130
+ process/resource inspection. One JSON response is emitted for each input line, which makes the
131
+ request/response stream directly replayable. See [docs/implementation.md](docs/implementation.md)
132
+ for the protocol boundary and current limitations.
133
+
134
+ For development, `make format`, `make lint`, and `make test` are the canonical local commands and
135
+ the exact entrypoints used by CI. See [CONTRIBUTING.md](CONTRIBUTING.md) for code, testing, review,
136
+ and optional pre-commit-hook guidelines.
137
+
138
+ The JSON report contains the exit status, typed stop reason, limits, aggregate usage, per-command
139
+ CPU/disk deltas, stdout, stderr, command trace, and unsupported capabilities.
140
+
141
+ `shellsim-python` and `serve --root` share one transactional importer. They treat the host path as
142
+ trusted harness input, reject symlinks, preserve permission bits, copy the project into `/work`,
143
+ then close that boundary before simulated execution starts. A Python directory automatically
144
+ discovers `test_*.py` files; `--entry FILE` selects a script within a directory. Use `--root` to
145
+ control which project tree is imported and the standard limit flags to constrain the run.
146
+
147
+ The PyPI package exposes `shellsim.run` for one fresh action and `shellsim.Environment` for a
148
+ persistent VFS, variables, processes, and cumulative resource budget. Results preserve stdout and
149
+ stderr as bytes and include resource, unsupported-capability, no-op, partial-command, and invocation
150
+ telemetry. `Environment.mount` is an explicit trusted-host operation with the same symlink rejection
151
+ and rollback behavior as the CLI importer. The extension never installs the standalone binaries'
152
+ process-wide seccomp filter, so importing or using it does not restrict the embedding Python
153
+ process. Simulated programs still execute through the capability-free Rust library and cannot
154
+ reach ambient host resources.
155
+
156
+ ## Virtual time
157
+
158
+ An environment owns deterministic monotonic, wall, and process-CPU clocks. Sleeps and deadlines
159
+ advance the event queue without blocking a host thread; VFS timestamps and Python observe the same
160
+ timeline. Runnable work has zero virtual duration and is bounded by CPU fuel. Background jobs,
161
+ pipelines, and nested shells run through the deterministic cooperative scheduler, so independent
162
+ sleeps overlap in virtual time. See
163
+ [docs/implementation.md](docs/implementation.md) for the state, scheduler, and replay contracts.
164
+
165
+ ## Persistent shell sessions
166
+
167
+ An `Environment` is a session, not a single command. Reusing it across `run_script_capture` calls
168
+ preserves the VFS, working directory, variables, arrays, functions, package state, clock/network
169
+ state, command history, and cumulative resource usage. CPU and output are cumulative fuel, disk
170
+ tracks current persistent usage, and temporary command memory is released while its peak remains.
171
+
172
+ `exit N`, `set -e` termination, CPU exhaustion, memory exhaustion, and output exhaustion make the
173
+ session terminal. Later calls return the same terminal outcome without executing or charging more
174
+ work. Disk-full errors are recoverable: a command can remove files and retry.
175
+
176
+ The `shell` subcommand drives one such environment line by line. It shows a prompt on a terminal,
177
+ preserves state between lines, exits normally on EOF or `exit`, and prints a reason before exiting
178
+ with status 137 when a resource is exhausted. It is an action console rather than a resumable
179
+ terminal: each completed action has closed stdin. Use a pipe or heredoc for command input. The
180
+ console collects a heredoc through its terminating delimiter before executing the action.
181
+
182
+ Invoking `python` without arguments transfers the foreground session to a deliberately-minimal
183
+ Python REPL. Simple assignments and expressions persist across actions; `exit()` or `quit()`
184
+ returns to the shell. This is a modeled process mode, not access to host CPython.
185
+
186
+ ## Bash-ish compatibility
187
+
188
+ The shell intentionally targets common agent-written Bash rather than the full Bash grammar. It
189
+ supports functions, indexed and associative arrays, `if`/`case`/`for`/`while`/`until`, C-style
190
+ `for ((...))` loops, `((...))`, pipelines, `&&`/`||`, background jobs, groups and subshells,
191
+ heredocs and here-strings, command/arithmetic substitution, brace expansion, parameter expansion,
192
+ globbing, `[[...]]`, and frequently used `set` options including `pipefail`.
193
+
194
+ Standard paths such as `/bin/sh` and `/usr/bin/env` resolve to their simulated commands. More
195
+ specialized Bash behavior, including process substitution, trap pseudo-events, coprocesses,
196
+ arbitrary process-group mutation, and some descriptor forms, remains outside the faithful subset. Logical children provide
197
+ isolated shell state, stable PIDs, overlapping virtual-time jobs, bounded pipes, `jobs`/`wait`,
198
+ default and caught signal delivery, `fg`/`bg` with STOP/CONT, dynamic `ps`, and generated `/proc`
199
+ views without creating host processes.
200
+
201
+ ## Command implementations
202
+
203
+ Commands receive a uniform environment context:
204
+
205
+ ```rust
206
+ fn run(env: &mut CommandContext<'_>, args: &[String], io: &mut Io) -> i32
207
+ ```
208
+
209
+ The dispatcher applies each command's coarse base CPU and memory cost. Commands add dynamic costs
210
+ when useful:
211
+
212
+ ```rust
213
+ if !env.reserve_memory(input.len() as u64 * 2) {
214
+ return 137;
215
+ }
216
+ if !env.charge_cpu(input.len() as u64) {
217
+ return 137;
218
+ }
219
+ ```
220
+
221
+ New commands should live in a focused module and use only the modeled command context. See
222
+ [docs/implementation.md](docs/implementation.md) for the integration checklist, trust levels,
223
+ resource rules, and the reason native compilers remain outside the simulation.
224
+
225
+ The current command set includes filesystem and text coreutils, `grep`, `sed`, a useful partial
226
+ `awk`, hashes and encoders, bounded tar and gzip tools, virtual `curl`/`wget`, deterministic Git and
227
+ Make subsets, shell builtins, minimal package/Python launchers, and simulated system queries such
228
+ as `env`, `printenv`, `uname`, `id`, `nproc`, `df`, `free`, and `ps`. Partial commands are surfaced
229
+ in evaluation reports instead of being presented as fully faithful implementations.
230
+
231
+ Disk enforcement lives inside `Vfs`, so direct command mutations cannot bypass capacity checks.
232
+ Commands should still surface `VfsError::NoSpace` with a non-zero status.
233
+
234
+ ## Python 3.14 compatibility
235
+
236
+ `python`, `python3`, and `python3.14` route to shellsim's safe in-process interpreter. Source goes
237
+ through a UTF-8/indentation-aware lexer, owned AST, semantic bytecode compiler, and metered stack
238
+ VM; host CPython is never invoked. The current language slice covers scalar and mutable containers,
239
+ comparisons and control flow, functions/closures/defaults/`*args`, classes and bound methods,
240
+ user inheritance with C3 lookup, `int` subclasses, constrained metaclasses, comprehensions,
241
+ suspended generators, exceptions and context managers, `assert`, decorators,
242
+ starred assignment/calls, f-strings, VFS-only imports, common iterator/container builtins, and the
243
+ modeled REPL/script/stdin/shebang entrypoints. Unsupported syntax and APIs fail loudly with a
244
+ diagnostic.
245
+
246
+ Native Python modules use an erased value ABI, checked object views, declarative type/module tables,
247
+ and narrow modeled capabilities. See [docs/python.md](docs/python.md) for the goals, value and
248
+ object model, extension workflow, compatibility evidence, and explicit frontiers.
249
+
250
+ The requested stdlib gate is 21/21 exact CPython 3.14 probes for these APIs: `sys.executable`,
251
+ `os.getenv`, `collections.defaultdict`, `itertools.count`/`islice`, `heapq.heapify`/`heappop`,
252
+ `bisect.bisect_left`, `math.sqrt`/`ceil`, `string.digits`, `json.dumps(sort_keys=...)`, `re.sub`,
253
+ `functools.reduce`, `dataclasses.dataclass`, `typing.List[...]`, `enum.Enum`,
254
+ `argparse.ArgumentParser.prog`, `csv.reader`/`writer`, source-backed `Counter`, `deque`, `json`,
255
+ `os.path`, `datetime`, byte-preserving `base64`, `hashlib`, `struct`, and `zlib`,
256
+ `import subprocess`, and the
257
+ `pytest`/`unittest.TestCase` entry points. These are intentionally partial module slices, not
258
+ claims of complete stdlib support.
259
+
260
+ `pytest` and `unittest` are VFS-only first runner slices: explicit files, stable definition-order
261
+ collection, plain zero-argument pytest tests, direct `unittest.TestCase` classes, tested assertions/
262
+ skip/raises controls, and bounded wrappers. Fixtures, decorated tests, plugins, rich
263
+ parametrization, async fixtures, directory/package discovery, and unlisted flags are rejected
264
+ explicitly. The 100-row TaskTrove mini corpus is differential-tested with per-row provenance (99
265
+ supported, one async frontier), and one complete `build-system-task-ordering` solution matches
266
+ CPython 3.14. CPU fuel, modeled memory, output, source/wrapper size, and nesting limits keep this
267
+ general-purpose slice safe and deliberately slow.
268
+
269
+ ## Library API
270
+
271
+ ```rust
272
+ use shellsim::{Environment, Limits};
273
+
274
+ let mut env = Environment::with_limits(Limits {
275
+ cpu: 100_000,
276
+ memory: 8 * 1024 * 1024,
277
+ disk: 2 * 1024 * 1024,
278
+ output: 64 * 1024,
279
+ });
280
+
281
+ let (outcome, stdout, stderr) = env.run_script_capture("echo hello");
282
+
283
+ // Harness actions may attach stdin without giving the simulated command host-terminal access.
284
+ let (outcome, stdout, stderr) =
285
+ env.run_script_capture_with_stdin("cat > input.txt", b"hello\n");
286
+ ```
287
+
288
+ An `Environment` preserves its VFS, working directory, variables, functions, options, and resource
289
+ usage across actions. Each action receives its own explicit stdin byte stream; an input redirect in
290
+ the action takes precedence. New environments include `/root`, `/tmp`, and `/work`.
291
+
292
+ `Interp` remains as an alias for `Environment` for source compatibility.
293
+
294
+ ## Layout
295
+
296
+ ```text
297
+ src/resources.rs limits, accounting, outcomes, command usage
298
+ src/interp.rs machine Environment and shell-local ProcessState
299
+ src/process.rs bounded logical process identities and lifecycle
300
+ src/pseudo_fs.rs generated read-only /proc and finite /dev views
301
+ src/vfs.rs quota-enforced in-memory filesystem
302
+ src/shell.rs lexer, parser, capture API
303
+ src/expand.rs shell expansion
304
+ src/exec.rs metered executor, pipelines, redirects, control flow
305
+ src/commands/ registry, command context, implementations
306
+ src/commands/awk.rs partial record-oriented awk
307
+ src/commands/system.rs simulated environment/system queries
308
+ src/python/ Python 3.14 lexer, parser, bytecode compiler, and metered VM
309
+ src/clock.rs virtual clock
310
+ src/net.rs virtual route-table network
311
+ docs/implementation.md architecture and command integration guide
312
+ docs/python.md Python goals, runtime model, and extension guide
313
+ docs/agent-environment.md reviewed agent-harness gaps and roadmap
314
+ ```
315
+
316
+ Run the unit and resource-invariant tests with `cargo test`.
317
+
@@ -0,0 +1,9 @@
1
+ shellsim/__init__.py,sha256=DE368umZqTSEh4Rof7Da-LTZI-6PNA2aiWhF3m0OKZ0,433
2
+ shellsim/_api.py,sha256=VnxqMUG-NpphsgPeOpm50KKUaGi5vG4typllfpuWrtY,9840
3
+ shellsim/_native.pyd,sha256=jxB6AadBlpOXDids3MD3RG6wWsIC0QA0AVEcMp4c0kE,6150144
4
+ shellsim/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
5
+ shellsim-0.1.0.dist-info/METADATA,sha256=mASGvpWb3Scvbicsc8jm_zsZw3pR4I7SpG9JEKZHulc,16794
6
+ shellsim-0.1.0.dist-info/WHEEL,sha256=xe4_tbg8wYdeh4O7nLbnWckzIQtCR4nODDgwke_TKy8,95
7
+ shellsim-0.1.0.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
8
+ shellsim-0.1.0.dist-info/sboms/shellsim-python-bindings.cyclonedx.json,sha256=EmpRQd1EXmrvMDHZh-pRJJOeiEuELg54geA1Qg3wdm4,67926
9
+ shellsim-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.15.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-abi3-win_amd64