shellsim 0.1.2__tar.gz → 0.1.4__tar.gz

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 (180) hide show
  1. {shellsim-0.1.2 → shellsim-0.1.4}/Cargo.lock +1 -1
  2. {shellsim-0.1.2 → shellsim-0.1.4}/Cargo.toml +5 -4
  3. shellsim-0.1.4/PKG-INFO +168 -0
  4. shellsim-0.1.4/README.md +144 -0
  5. {shellsim-0.1.2 → shellsim-0.1.4}/pyproject.toml +26 -3
  6. {shellsim-0.1.2 → shellsim-0.1.4}/python/native/Cargo.lock +2 -2
  7. {shellsim-0.1.2 → shellsim-0.1.4}/python/native/Cargo.toml +4 -3
  8. {shellsim-0.1.2 → shellsim-0.1.4}/python/native/src/lib.rs +128 -17
  9. {shellsim-0.1.2 → shellsim-0.1.4}/python/shellsim/__init__.py +6 -0
  10. {shellsim-0.1.2 → shellsim-0.1.4}/python/shellsim/__main__.py +0 -1
  11. {shellsim-0.1.2 → shellsim-0.1.4}/python/shellsim/_api.py +127 -19
  12. {shellsim-0.1.2 → shellsim-0.1.4}/python/shellsim/_cli.py +4 -11
  13. shellsim-0.1.4/python/shellsim/python.py +32 -0
  14. shellsim-0.1.4/src/commands/awk/ast.rs +146 -0
  15. shellsim-0.1.4/src/commands/awk/eval.rs +1180 -0
  16. shellsim-0.1.4/src/commands/awk/lexer.rs +367 -0
  17. shellsim-0.1.4/src/commands/awk/parser.rs +522 -0
  18. shellsim-0.1.4/src/commands/awk.rs +243 -0
  19. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/builtins.rs +453 -112
  20. shellsim-0.1.4/src/commands/dd.rs +308 -0
  21. shellsim-0.1.4/src/commands/find.rs +323 -0
  22. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/fs.rs +373 -178
  23. shellsim-0.1.4/src/commands/git/apply.rs +255 -0
  24. shellsim-0.1.4/src/commands/git/commit.rs +339 -0
  25. shellsim-0.1.4/src/commands/git/compare.rs +731 -0
  26. shellsim-0.1.4/src/commands/git/config.rs +356 -0
  27. shellsim-0.1.4/src/commands/git/conflict.rs +564 -0
  28. shellsim-0.1.4/src/commands/git/diff.rs +609 -0
  29. shellsim-0.1.4/src/commands/git/ignore.rs +381 -0
  30. shellsim-0.1.4/src/commands/git/log.rs +1055 -0
  31. shellsim-0.1.4/src/commands/git/merge.rs +722 -0
  32. shellsim-0.1.4/src/commands/git/mod.rs +794 -0
  33. shellsim-0.1.4/src/commands/git/plumbing.rs +1215 -0
  34. shellsim-0.1.4/src/commands/git/rebase.rs +475 -0
  35. shellsim-0.1.4/src/commands/git/refs.rs +699 -0
  36. shellsim-0.1.4/src/commands/git/repo.rs +1210 -0
  37. shellsim-0.1.4/src/commands/git/stash.rs +441 -0
  38. shellsim-0.1.4/src/commands/git/switch.rs +522 -0
  39. shellsim-0.1.4/src/commands/git/worktree.rs +1592 -0
  40. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/hashing.rs +76 -9
  41. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/mod.rs +133 -6
  42. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/net.rs +14 -4
  43. shellsim-0.1.4/src/commands/options.rs +271 -0
  44. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/patch.rs +100 -23
  45. shellsim-0.1.4/src/commands/pkg.rs +304 -0
  46. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/printf.rs +45 -9
  47. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/proc.rs +220 -92
  48. shellsim-0.1.4/src/commands/regex_compat.rs +56 -0
  49. shellsim-0.1.4/src/commands/sed.rs +787 -0
  50. shellsim-0.1.4/src/commands/sort.rs +255 -0
  51. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/system.rs +120 -26
  52. shellsim-0.1.4/src/commands/text.rs +1980 -0
  53. shellsim-0.1.4/src/commands/unavailable.rs +39 -0
  54. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/util.rs +37 -2
  55. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/zipcmd.rs +12 -1
  56. {shellsim-0.1.2 → shellsim-0.1.4}/src/descriptors.rs +121 -0
  57. {shellsim-0.1.2 → shellsim-0.1.4}/src/exec.rs +205 -32
  58. {shellsim-0.1.2 → shellsim-0.1.4}/src/expand.rs +64 -7
  59. {shellsim-0.1.2 → shellsim-0.1.4}/src/harness.rs +3 -3
  60. {shellsim-0.1.2 → shellsim-0.1.4}/src/interp.rs +228 -9
  61. shellsim-0.1.4/src/jqcmd.rs +1497 -0
  62. {shellsim-0.1.2 → shellsim-0.1.4}/src/main.rs +3 -3
  63. shellsim-0.1.4/src/net.rs +550 -0
  64. shellsim-0.1.4/src/netcmd.rs +357 -0
  65. {shellsim-0.1.2 → shellsim-0.1.4}/src/pseudo_fs.rs +20 -2
  66. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/ast.rs +16 -3
  67. shellsim-0.1.4/src/python/bytecode.rs +511 -0
  68. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/compiler.rs +284 -75
  69. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/filesystem.rs +81 -32
  70. shellsim-0.1.4/src/python/heap.rs +1795 -0
  71. shellsim-0.1.4/src/python/http.rs +135 -0
  72. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/lexer.rs +48 -3
  73. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/mod.rs +302 -46
  74. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/native.rs +260 -39
  75. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/number.rs +224 -80
  76. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/object_model.rs +294 -65
  77. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/parser.rs +324 -69
  78. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/protocol.rs +179 -14
  79. shellsim-0.1.4/src/python/slice.rs +133 -0
  80. shellsim-0.1.4/src/python/stdlib/argparse.rs +674 -0
  81. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/core.rs +596 -25
  82. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/frozen.rs +10 -0
  83. shellsim-0.1.4/src/python/stdlib/http.rs +63 -0
  84. shellsim-0.1.4/src/python/stdlib/importlib.rs +43 -0
  85. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/json.rs +36 -5
  86. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/math.rs +36 -0
  87. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/mod.rs +12 -1
  88. shellsim-0.1.4/src/python/stdlib/numpy.rs +4730 -0
  89. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/os.rs +15 -6
  90. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/csv.py +4 -0
  91. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/datetime.py +15 -0
  92. shellsim-0.1.4/src/python/stdlib/source/http.py +3 -0
  93. shellsim-0.1.4/src/python/stdlib/source/http_client.py +271 -0
  94. shellsim-0.1.4/src/python/stdlib/source/importlib.py +47 -0
  95. shellsim-0.1.4/src/python/stdlib/source/json.py +25 -0
  96. shellsim-0.1.4/src/python/stdlib/source/numpy_random.py +99 -0
  97. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/os.py +81 -8
  98. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/pathlib.py +36 -0
  99. shellsim-0.1.4/src/python/stdlib/source/random.py +97 -0
  100. shellsim-0.1.4/src/python/stdlib/source/statistics.py +127 -0
  101. shellsim-0.1.4/src/python/stdlib/source/urllib.py +4 -0
  102. shellsim-0.1.4/src/python/stdlib/source/urllib_error.py +50 -0
  103. shellsim-0.1.4/src/python/stdlib/source/urllib_request.py +155 -0
  104. shellsim-0.1.4/src/python/stdlib/source/zipfile.py +234 -0
  105. shellsim-0.1.4/src/python/stdlib/sys.rs +183 -0
  106. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/vfs.rs +46 -0
  107. shellsim-0.1.4/src/python/string.rs +170 -0
  108. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/token.rs +5 -0
  109. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/vm.rs +2720 -761
  110. {shellsim-0.1.2 → shellsim-0.1.4}/src/resources.rs +10 -0
  111. {shellsim-0.1.2 → shellsim-0.1.4}/src/scenario.rs +5 -5
  112. {shellsim-0.1.2 → shellsim-0.1.4}/src/shell.rs +142 -12
  113. {shellsim-0.1.2 → shellsim-0.1.4}/src/telemetry.rs +2 -2
  114. shellsim-0.1.2/PKG-INFO +0 -331
  115. shellsim-0.1.2/README.md +0 -307
  116. shellsim-0.1.2/src/commands/awk.rs +0 -674
  117. shellsim-0.1.2/src/commands/git.rs +0 -1858
  118. shellsim-0.1.2/src/commands/pkg.rs +0 -223
  119. shellsim-0.1.2/src/commands/sort.rs +0 -55
  120. shellsim-0.1.2/src/commands/text.rs +0 -1148
  121. shellsim-0.1.2/src/jqcmd.rs +0 -352
  122. shellsim-0.1.2/src/net.rs +0 -218
  123. shellsim-0.1.2/src/netcmd.rs +0 -135
  124. shellsim-0.1.2/src/python/bytecode.rs +0 -107
  125. shellsim-0.1.2/src/python/heap.rs +0 -569
  126. shellsim-0.1.2/src/python/stdlib/argparse.rs +0 -265
  127. shellsim-0.1.2/src/python/stdlib/source/json.py +0 -15
  128. shellsim-0.1.2/src/python/stdlib/sys.rs +0 -89
  129. {shellsim-0.1.2 → shellsim-0.1.4}/LICENSE +0 -0
  130. {shellsim-0.1.2 → shellsim-0.1.4}/python/shellsim/py.typed +0 -0
  131. {shellsim-0.1.2 → shellsim-0.1.4}/rust-toolchain.toml +0 -0
  132. {shellsim-0.1.2 → shellsim-0.1.4}/src/bin/shellsim-python.rs +0 -0
  133. {shellsim-0.1.2 → shellsim-0.1.4}/src/clock.rs +0 -0
  134. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/archives.rs +0 -0
  135. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/echo.rs +0 -0
  136. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/makecmd.rs +0 -0
  137. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/rgcmd.rs +0 -0
  138. {shellsim-0.1.2 → shellsim-0.1.4}/src/commands/tarcmd.rs +0 -0
  139. {shellsim-0.1.2 → shellsim-0.1.4}/src/harness_manager.rs +0 -0
  140. {shellsim-0.1.2 → shellsim-0.1.4}/src/hashes.rs +0 -0
  141. {shellsim-0.1.2 → shellsim-0.1.4}/src/host_ingest.rs +0 -0
  142. {shellsim-0.1.2 → shellsim-0.1.4}/src/lib.rs +0 -0
  143. {shellsim-0.1.2 → shellsim-0.1.4}/src/mcp.rs +0 -0
  144. {shellsim-0.1.2 → shellsim-0.1.4}/src/process.rs +0 -0
  145. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/process.rs +0 -0
  146. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/source.rs +0 -0
  147. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/base64.rs +0 -0
  148. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/bisect.rs +0 -0
  149. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/collections.rs +0 -0
  150. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/dataclasses.rs +0 -0
  151. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/enum.rs +0 -0
  152. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/functools.rs +0 -0
  153. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/hashlib.rs +0 -0
  154. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/heapq.rs +0 -0
  155. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/itertools.rs +0 -0
  156. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/pytest.rs +0 -0
  157. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/re.rs +0 -0
  158. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/abc.py +0 -0
  159. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/base64.py +0 -0
  160. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/codecs.py +0 -0
  161. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/collections.py +0 -0
  162. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/glob.py +0 -0
  163. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/hashlib.py +0 -0
  164. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/io.py +0 -0
  165. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/logging.py +0 -0
  166. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/struct.py +0 -0
  167. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/subprocess.py +0 -0
  168. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/tempfile.py +0 -0
  169. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/uuid.py +0 -0
  170. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/source/zlib.py +0 -0
  171. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/string.rs +0 -0
  172. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/struct.rs +0 -0
  173. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/subprocess.rs +0 -0
  174. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/time.rs +0 -0
  175. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/typing.rs +0 -0
  176. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/unittest.rs +0 -0
  177. {shellsim-0.1.2 → shellsim-0.1.4}/src/python/stdlib/zlib.rs +0 -0
  178. {shellsim-0.1.2 → shellsim-0.1.4}/src/sandbox.rs +0 -0
  179. {shellsim-0.1.2 → shellsim-0.1.4}/src/scheduler.rs +0 -0
  180. {shellsim-0.1.2 → shellsim-0.1.4}/src/vfs.rs +0 -0
@@ -330,7 +330,7 @@ dependencies = [
330
330
 
331
331
  [[package]]
332
332
  name = "shellsim"
333
- version = "0.1.2"
333
+ version = "0.1.4"
334
334
  dependencies = [
335
335
  "base64",
336
336
  "crc32fast",
@@ -1,8 +1,8 @@
1
1
  [package]
2
2
  name = "shellsim"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  edition = "2021"
5
- description = "Deterministic, resource-constrained BusyBox-like environment for evaluating agents"
5
+ description = "In-process BusyBox-like environment for RL rollouts and agent tests"
6
6
  license = "Apache-2.0"
7
7
  readme = "README.md"
8
8
  repository = "https://github.com/rjpower/shellsim"
@@ -58,5 +58,6 @@ python = []
58
58
  opt-level = 1
59
59
 
60
60
  [profile.release]
61
- opt-level = "s"
62
- lto = false
61
+ opt-level = "z"
62
+ lto = "fat"
63
+ codegen-units = 1
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: shellsim
3
+ Version: 0.1.4
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: In-process BusyBox-like environment for RL rollouts and agent tests
16
+ Keywords: agents,containers,reinforcement-learning,sandbox,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 BusyBox for containers: one small, deterministic process that provides a useful
27
+ Unix-shaped environment without starting a VM, container runtime, or host subprocess. It is built
28
+ for experimentation and testing with reinforcement-learning rollouts and agentic environments,
29
+ where fast startup, reproducibility, isolation, and explicit resource limits matter more than
30
+ cycle-accurate emulation.
31
+
32
+ Shell programs, common command-line tools, logical processes, and Python run in-process against an
33
+ in-memory filesystem. Simulated code cannot access the host filesystem, processes, network,
34
+ environment, or clock. A trusted harness may copy a selected project into the virtual filesystem
35
+ before execution; changes never write back to the host.
36
+
37
+ ## Compatibility
38
+
39
+ Shellsim aims for broad compatibility inside clear boundaries. A supported facility should handle
40
+ almost all ordinary uses, even when obscure flags or legacy behavior remain out of scope. A module
41
+ or command with no coherent useful subset is omitted instead of being exposed as a misleading
42
+ stub. Unsupported syntax, options, executable formats, and capabilities fail visibly and are
43
+ included in structured results.
44
+
45
+ The current environment includes:
46
+
47
+ - a Bash-like shell with pipelines, redirections, functions, common expansions, control flow,
48
+ background jobs, signals, and job control;
49
+ - common filesystem, text, archive, Git, Make, process, and system commands;
50
+ - deterministic virtual time, network fixtures, `/proc`, `/dev`, processes, descriptors, and
51
+ bounded pipes;
52
+ - a mostly complete Python language runtime with a deliberately selected standard-library and
53
+ third-party module surface.
54
+
55
+ Python is source-compatible where supported, not ABI-compatible with CPython. Native extensions,
56
+ network package installation, compilers, and arbitrary machine code are outside the simulation
57
+ boundary. The simulated `pip` and `uv` paths can activate packages already bundled with
58
+ shellsim; they reject other packages instead of fetching them. See
59
+ [Python in shellsim](docs/python.md) for the current contract.
60
+
61
+ `git` provides a porcelain subset backed by the virtual filesystem: staging, commits, history,
62
+ diffs, branches, tags, merges, stashes, and search, with output that matches real Git where an
63
+ agent is likely to parse it. Networked subcommands and content conflicts are refused rather than
64
+ approximated. See [Git in shellsim](docs/git.md) for the supported surface and its boundaries.
65
+
66
+ Common utilities accept clustered short options, long options, option values, and `--` through a
67
+ shared parser. Each utility declares its supported options. For example, `grep` supports ordinary
68
+ basic and extended regular expressions, recursive search, fixed strings, word and whole-line
69
+ matching, pattern files, include and exclude filters, context, counts, line numbers, match limits,
70
+ and common output controls. `sed` covers addresses and ranges plus the common substitution,
71
+ selection, text, transliteration, and early-exit commands. `awk` parses a typed language subset
72
+ with record rules, control flow, fields, associative arrays, arithmetic, regular expressions, and
73
+ the usual scalar functions. Unsupported syntax and options exit nonzero with a direct diagnostic;
74
+ text processing is UTF-8-only unless a command documents a byte-oriented mode.
75
+
76
+ ## Install and run
77
+
78
+ Install the Python package and console command:
79
+
80
+ ```sh
81
+ python -m pip install shellsim
82
+ shellsim -c 'printf "b\na\n" | sort'
83
+ shellsim --root ./project -c 'python3.14 test.py'
84
+ ```
85
+
86
+ `--root` copies the selected host tree into a disposable `/work` snapshot. With no `-c` and a
87
+ terminal attached, `shellsim` starts a persistent interactive session.
88
+
89
+ To build the Rust binaries from source:
90
+
91
+ ```sh
92
+ cargo build --release
93
+ ./target/release/shellsim -c 'echo hello'
94
+ ./target/release/shellsim eval --cpu 100k --memory 8m -c 'make test'
95
+ ./target/release/shellsim-python ./project/main.py -- arg1
96
+ ```
97
+
98
+ Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
99
+ exit status, stdout and stderr, resource use, command trace, and unsupported behavior.
100
+ `unsupported_commands` lists recognized or attempted commands that crossed the capability
101
+ boundary; invocation records include trust, status, and the unsupported reason.
102
+
103
+ The Python API exposes fresh and persistent environments:
104
+
105
+ ```python
106
+ import shellsim
107
+
108
+ environment = shellsim.Environment(cpu=100_000)
109
+ environment.write_file("/work/main.py", "print(6 * 7)\n")
110
+ result = environment.run("python3.14 /work/main.py")
111
+ assert result.returncode == 0
112
+ assert result.stdout == b"42\n"
113
+ ```
114
+
115
+ Static HTTP fixtures use the same isolated environment. They do not enable sockets, DNS, TLS, or
116
+ host-network access:
117
+
118
+ ```python
119
+ environment = shellsim.Environment(http={
120
+ "https://api.test/items/*": shellsim.HttpResponse(
121
+ status=200,
122
+ headers={"Content-Type": "application/json"},
123
+ body='{"items": []}',
124
+ ),
125
+ })
126
+ result = environment.run("curl -s https://api.test/items/1")
127
+
128
+ result = environment.run_python("""
129
+ from urllib.request import urlopen
130
+ print(urlopen('https://api.test/items/1').read())
131
+ """)
132
+ assert result.network_requests[0].matched
133
+ ```
134
+
135
+ Python source can bypass shell parsing and quoting while using the same isolated runtime:
136
+
137
+ ```python
138
+ result = shellsim.python.run("print(sum(range(10)))", argv=["example"])
139
+ result.check_returncode()
140
+
141
+ environment = shellsim.Environment()
142
+ result = environment.run_python("print('persistent VFS, fresh Python interpreter')")
143
+ ```
144
+
145
+ ## Agent harness
146
+
147
+ `shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
148
+ bounded execution, streaming actions, VFS operations, checkpoints, workspace diffs, process and
149
+ resource inspection, and deterministic session forks. `shellsim mcp` exposes the same environment
150
+ as a stdio MCP server. `shellsim replay scenario.ndjson` reruns checked action transcripts.
151
+
152
+ ```sh
153
+ printf '%s\n' \
154
+ '{"id":1,"op":"execute","source":"printf hello > result"}' \
155
+ '{"id":2,"op":"workspace_diff"}' \
156
+ | shellsim serve --root ./project
157
+ ```
158
+
159
+ ## Resource model
160
+
161
+ CPU is deterministic fuel, memory is modeled working set, disk is current virtual-filesystem
162
+ usage, and output bounds materialized stdout and stderr. The defaults are 10,000,000 CPU units,
163
+ 64 MiB memory, 64 MiB disk, and 4 MiB output. Costs are stable and intentionally approximate.
164
+ Exhaustion is observable and never falls back to an ambient host implementation.
165
+
166
+ For internals and contribution workflow, see [implementation](docs/implementation.md),
167
+ [Python](docs/python.md), and [CONTRIBUTING.md](CONTRIBUTING.md).
168
+
@@ -0,0 +1,144 @@
1
+ # shellsim
2
+
3
+ Shellsim is a BusyBox for containers: one small, deterministic process that provides a useful
4
+ Unix-shaped environment without starting a VM, container runtime, or host subprocess. It is built
5
+ for experimentation and testing with reinforcement-learning rollouts and agentic environments,
6
+ where fast startup, reproducibility, isolation, and explicit resource limits matter more than
7
+ cycle-accurate emulation.
8
+
9
+ Shell programs, common command-line tools, logical processes, and Python run in-process against an
10
+ in-memory filesystem. Simulated code cannot access the host filesystem, processes, network,
11
+ environment, or clock. A trusted harness may copy a selected project into the virtual filesystem
12
+ before execution; changes never write back to the host.
13
+
14
+ ## Compatibility
15
+
16
+ Shellsim aims for broad compatibility inside clear boundaries. A supported facility should handle
17
+ almost all ordinary uses, even when obscure flags or legacy behavior remain out of scope. A module
18
+ or command with no coherent useful subset is omitted instead of being exposed as a misleading
19
+ stub. Unsupported syntax, options, executable formats, and capabilities fail visibly and are
20
+ included in structured results.
21
+
22
+ The current environment includes:
23
+
24
+ - a Bash-like shell with pipelines, redirections, functions, common expansions, control flow,
25
+ background jobs, signals, and job control;
26
+ - common filesystem, text, archive, Git, Make, process, and system commands;
27
+ - deterministic virtual time, network fixtures, `/proc`, `/dev`, processes, descriptors, and
28
+ bounded pipes;
29
+ - a mostly complete Python language runtime with a deliberately selected standard-library and
30
+ third-party module surface.
31
+
32
+ Python is source-compatible where supported, not ABI-compatible with CPython. Native extensions,
33
+ network package installation, compilers, and arbitrary machine code are outside the simulation
34
+ boundary. The simulated `pip` and `uv` paths can activate packages already bundled with
35
+ shellsim; they reject other packages instead of fetching them. See
36
+ [Python in shellsim](docs/python.md) for the current contract.
37
+
38
+ `git` provides a porcelain subset backed by the virtual filesystem: staging, commits, history,
39
+ diffs, branches, tags, merges, stashes, and search, with output that matches real Git where an
40
+ agent is likely to parse it. Networked subcommands and content conflicts are refused rather than
41
+ approximated. See [Git in shellsim](docs/git.md) for the supported surface and its boundaries.
42
+
43
+ Common utilities accept clustered short options, long options, option values, and `--` through a
44
+ shared parser. Each utility declares its supported options. For example, `grep` supports ordinary
45
+ basic and extended regular expressions, recursive search, fixed strings, word and whole-line
46
+ matching, pattern files, include and exclude filters, context, counts, line numbers, match limits,
47
+ and common output controls. `sed` covers addresses and ranges plus the common substitution,
48
+ selection, text, transliteration, and early-exit commands. `awk` parses a typed language subset
49
+ with record rules, control flow, fields, associative arrays, arithmetic, regular expressions, and
50
+ the usual scalar functions. Unsupported syntax and options exit nonzero with a direct diagnostic;
51
+ text processing is UTF-8-only unless a command documents a byte-oriented mode.
52
+
53
+ ## Install and run
54
+
55
+ Install the Python package and console command:
56
+
57
+ ```sh
58
+ python -m pip install shellsim
59
+ shellsim -c 'printf "b\na\n" | sort'
60
+ shellsim --root ./project -c 'python3.14 test.py'
61
+ ```
62
+
63
+ `--root` copies the selected host tree into a disposable `/work` snapshot. With no `-c` and a
64
+ terminal attached, `shellsim` starts a persistent interactive session.
65
+
66
+ To build the Rust binaries from source:
67
+
68
+ ```sh
69
+ cargo build --release
70
+ ./target/release/shellsim -c 'echo hello'
71
+ ./target/release/shellsim eval --cpu 100k --memory 8m -c 'make test'
72
+ ./target/release/shellsim-python ./project/main.py -- arg1
73
+ ```
74
+
75
+ Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
76
+ exit status, stdout and stderr, resource use, command trace, and unsupported behavior.
77
+ `unsupported_commands` lists recognized or attempted commands that crossed the capability
78
+ boundary; invocation records include trust, status, and the unsupported reason.
79
+
80
+ The Python API exposes fresh and persistent environments:
81
+
82
+ ```python
83
+ import shellsim
84
+
85
+ environment = shellsim.Environment(cpu=100_000)
86
+ environment.write_file("/work/main.py", "print(6 * 7)\n")
87
+ result = environment.run("python3.14 /work/main.py")
88
+ assert result.returncode == 0
89
+ assert result.stdout == b"42\n"
90
+ ```
91
+
92
+ Static HTTP fixtures use the same isolated environment. They do not enable sockets, DNS, TLS, or
93
+ host-network access:
94
+
95
+ ```python
96
+ environment = shellsim.Environment(http={
97
+ "https://api.test/items/*": shellsim.HttpResponse(
98
+ status=200,
99
+ headers={"Content-Type": "application/json"},
100
+ body='{"items": []}',
101
+ ),
102
+ })
103
+ result = environment.run("curl -s https://api.test/items/1")
104
+
105
+ result = environment.run_python("""
106
+ from urllib.request import urlopen
107
+ print(urlopen('https://api.test/items/1').read())
108
+ """)
109
+ assert result.network_requests[0].matched
110
+ ```
111
+
112
+ Python source can bypass shell parsing and quoting while using the same isolated runtime:
113
+
114
+ ```python
115
+ result = shellsim.python.run("print(sum(range(10)))", argv=["example"])
116
+ result.check_returncode()
117
+
118
+ environment = shellsim.Environment()
119
+ result = environment.run_python("print('persistent VFS, fresh Python interpreter')")
120
+ ```
121
+
122
+ ## Agent harness
123
+
124
+ `shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
125
+ bounded execution, streaming actions, VFS operations, checkpoints, workspace diffs, process and
126
+ resource inspection, and deterministic session forks. `shellsim mcp` exposes the same environment
127
+ as a stdio MCP server. `shellsim replay scenario.ndjson` reruns checked action transcripts.
128
+
129
+ ```sh
130
+ printf '%s\n' \
131
+ '{"id":1,"op":"execute","source":"printf hello > result"}' \
132
+ '{"id":2,"op":"workspace_diff"}' \
133
+ | shellsim serve --root ./project
134
+ ```
135
+
136
+ ## Resource model
137
+
138
+ CPU is deterministic fuel, memory is modeled working set, disk is current virtual-filesystem
139
+ usage, and output bounds materialized stdout and stderr. The defaults are 10,000,000 CPU units,
140
+ 64 MiB memory, 64 MiB disk, and 4 MiB output. Costs are stable and intentionally approximate.
141
+ Exhaustion is observable and never falls back to an ambient host implementation.
142
+
143
+ For internals and contribution workflow, see [implementation](docs/implementation.md),
144
+ [Python](docs/python.md), and [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -5,7 +5,7 @@ build-backend = "maturin"
5
5
  [project]
6
6
  name = "shellsim"
7
7
  dynamic = ["version"]
8
- description = "Deterministic, resource-constrained shell and Python execution for tests"
8
+ description = "In-process BusyBox-like environment for RL rollouts and agent tests"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = { file = "LICENSE" }
@@ -22,7 +22,7 @@ classifiers = [
22
22
  "Topic :: Software Development :: Testing",
23
23
  "Typing :: Typed",
24
24
  ]
25
- keywords = ["sandbox", "shell", "simulation", "testing"]
25
+ keywords = ["agents", "containers", "reinforcement-learning", "sandbox", "simulation", "testing"]
26
26
 
27
27
  [project.urls]
28
28
  Homepage = "https://github.com/rjpower/shellsim"
@@ -44,4 +44,27 @@ locked = true
44
44
  strip = true
45
45
 
46
46
  [tool.pytest.ini_options]
47
- testpaths = ["python_tests"]
47
+ testpaths = ["tests/python_package", "tests/tooling"]
48
+
49
+ [tool.marin-style]
50
+ checks = ["ruff-check", "ruff-format"]
51
+ include = [
52
+ "infra/*.py",
53
+ "infra/**/*.py",
54
+ "python/**/*.py",
55
+ "tests/python/test_*.py",
56
+ "tests/python_package/**/*.py",
57
+ "tests/tooling/**/*.py",
58
+ "tools/*.py",
59
+ "tools/**/*.py",
60
+ ]
61
+ main_branch = "main"
62
+ ruff_version = "0.14.3"
63
+
64
+ [tool.ruff]
65
+ line-length = 120
66
+ target-version = "py39"
67
+
68
+ [tool.ruff.lint]
69
+ select = ["E", "F", "I", "B", "C4"]
70
+ ignore = ["E501"]
@@ -406,7 +406,7 @@ dependencies = [
406
406
 
407
407
  [[package]]
408
408
  name = "shellsim"
409
- version = "0.1.2"
409
+ version = "0.1.4"
410
410
  dependencies = [
411
411
  "base64",
412
412
  "crc32fast",
@@ -427,7 +427,7 @@ dependencies = [
427
427
 
428
428
  [[package]]
429
429
  name = "shellsim-python-bindings"
430
- version = "0.1.2"
430
+ version = "0.1.4"
431
431
  dependencies = [
432
432
  "pyo3",
433
433
  "serde",
@@ -2,7 +2,7 @@
2
2
 
3
3
  [package]
4
4
  name = "shellsim-python-bindings"
5
- version = "0.1.2"
5
+ version = "0.1.4"
6
6
  edition = "2021"
7
7
  description = "Python bindings for shellsim"
8
8
  license = "Apache-2.0"
@@ -20,5 +20,6 @@ serde_json = "1"
20
20
  shellsim = { path = "../.." }
21
21
 
22
22
  [profile.release]
23
- opt-level = "s"
24
- lto = false
23
+ opt-level = "z"
24
+ lto = "fat"
25
+ codegen-units = 1
@@ -17,6 +17,7 @@ use pyo3::exceptions::PyException;
17
17
  use pyo3::prelude::*;
18
18
  use pyo3::types::PyBytes;
19
19
  use serde::Serialize;
20
+ use shellsim::net::NetworkRequest;
20
21
  use shellsim::{CommandTrust, InvocationEvent, Limits, RunOutcome};
21
22
 
22
23
  const ACTION_STACK_BYTES: usize = 8 * 1024 * 1024;
@@ -35,10 +36,12 @@ struct RunMetadata {
35
36
  dropped_unsupported: u64,
36
37
  commands: Vec<String>,
37
38
  dropped_commands: u64,
38
- noop_commands: Vec<String>,
39
+ unsupported_commands: Vec<String>,
39
40
  partial_commands: Vec<String>,
40
41
  invocations: Vec<InvocationEvent>,
41
42
  dropped_invocations: u64,
43
+ network_requests: Vec<NetworkRequest>,
44
+ dropped_network_requests: u64,
42
45
  }
43
46
 
44
47
  #[derive(Serialize)]
@@ -47,6 +50,17 @@ struct MountMetadata {
47
50
  skipped_directories: Vec<String>,
48
51
  }
49
52
 
53
+ struct MetadataStart {
54
+ command: usize,
55
+ dropped_commands: u64,
56
+ unsupported: usize,
57
+ dropped_unsupported: u64,
58
+ invocation: u64,
59
+ dropped_invocations: u64,
60
+ network: usize,
61
+ dropped_network: u64,
62
+ }
63
+
50
64
  /// One persistent simulated machine owned by Python.
51
65
  #[pyclass(module = "shellsim._native")]
52
66
  struct NativeEnvironment {
@@ -91,6 +105,31 @@ impl NativeEnvironment {
91
105
  ))
92
106
  }
93
107
 
108
+ /// Execute Python source directly without passing it through the shell parser.
109
+ fn run_python(
110
+ &self,
111
+ py: Python<'_>,
112
+ source: String,
113
+ argv: Vec<String>,
114
+ stdin: Vec<u8>,
115
+ ) -> PyResult<(String, Py<PyBytes>, Py<PyBytes>)> {
116
+ let (metadata, stdout, stderr) = py
117
+ .detach(|| {
118
+ let mut environment = self.lock_environment()?;
119
+ on_worker(&mut environment, move |environment| {
120
+ Ok(run_python_action(environment, source, argv, stdin))
121
+ })
122
+ })
123
+ .map_err(SimulationError::new_err)?;
124
+ let metadata = serde_json::to_string(&metadata)
125
+ .map_err(|error| SimulationError::new_err(error.to_string()))?;
126
+ Ok((
127
+ metadata,
128
+ PyBytes::new(py, &stdout).unbind(),
129
+ PyBytes::new(py, &stderr).unbind(),
130
+ ))
131
+ }
132
+
94
133
  /// Write one byte string into the simulated filesystem.
95
134
  fn write_file(&self, py: Python<'_>, path: String, data: Vec<u8>, mode: u32) -> PyResult<()> {
96
135
  py.detach(|| {
@@ -139,6 +178,36 @@ impl NativeEnvironment {
139
178
  .map_err(SimulationError::new_err)
140
179
  }
141
180
 
181
+ /// Register one static response in the simulated HTTP broker.
182
+ fn route_http(
183
+ &self,
184
+ py: Python<'_>,
185
+ pattern: String,
186
+ method: Option<String>,
187
+ status: u16,
188
+ headers: Vec<(String, String)>,
189
+ body: Vec<u8>,
190
+ ) -> PyResult<()> {
191
+ py.detach(|| {
192
+ let mut environment = self.lock_environment()?;
193
+ on_worker(&mut environment, move |environment| {
194
+ environment
195
+ .net
196
+ .route(
197
+ &pattern,
198
+ method.as_deref(),
199
+ shellsim::net::HttpResponse {
200
+ status,
201
+ headers,
202
+ body,
203
+ },
204
+ )
205
+ .map_err(|error| error.to_string())
206
+ })
207
+ })
208
+ .map_err(SimulationError::new_err)
209
+ }
210
+
142
211
  /// Import one explicitly selected trusted host tree into the simulated filesystem.
143
212
  fn mount(
144
213
  &self,
@@ -188,35 +257,77 @@ fn run_action(
188
257
  source: &str,
189
258
  stdin: &[u8],
190
259
  ) -> (RunMetadata, Vec<u8>, Vec<u8>) {
191
- let command_start = environment.cmd_trace.len();
192
- let dropped_commands_start = environment.cmd_trace.dropped();
193
- let unsupported_start = environment.unsupported.len();
194
- let dropped_unsupported_start = environment.unsupported.dropped();
195
- let invocation_start = environment.invocations.next_sequence();
196
- let dropped_invocations_start = environment.invocations.dropped();
260
+ let start = metadata_start(environment);
197
261
  let (outcome, stdout, stderr) = environment.run_script_capture_with_stdin(source, stdin);
198
- let invocations = environment.invocations.events_since(invocation_start);
199
- let metadata = RunMetadata {
262
+ (metadata_finish(environment, outcome, start), stdout, stderr)
263
+ }
264
+
265
+ fn run_python_action(
266
+ environment: &mut shellsim::Environment,
267
+ source: String,
268
+ arguments: Vec<String>,
269
+ stdin: Vec<u8>,
270
+ ) -> (RunMetadata, Vec<u8>, Vec<u8>) {
271
+ let start = metadata_start(environment);
272
+ let mut argv = vec!["python3.14".to_string(), "-c".to_string(), source];
273
+ argv.extend(arguments);
274
+ let mut stdout = Vec::new();
275
+ let mut stderr = Vec::new();
276
+ let status = shellsim::exec::exec(
277
+ environment,
278
+ &shellsim::shell::Node::ArgvCommand(argv),
279
+ stdin,
280
+ &mut stdout,
281
+ &mut stderr,
282
+ );
283
+ let outcome = environment.outcome(status);
284
+ (metadata_finish(environment, outcome, start), stdout, stderr)
285
+ }
286
+
287
+ fn metadata_start(environment: &shellsim::Environment) -> MetadataStart {
288
+ MetadataStart {
289
+ command: environment.cmd_trace.len(),
290
+ dropped_commands: environment.cmd_trace.dropped(),
291
+ unsupported: environment.unsupported.len(),
292
+ dropped_unsupported: environment.unsupported.dropped(),
293
+ invocation: environment.invocations.next_sequence(),
294
+ dropped_invocations: environment.invocations.dropped(),
295
+ network: environment.net.log.len(),
296
+ dropped_network: environment.net.dropped_requests,
297
+ }
298
+ }
299
+
300
+ fn metadata_finish(
301
+ environment: &shellsim::Environment,
302
+ outcome: RunOutcome,
303
+ start: MetadataStart,
304
+ ) -> RunMetadata {
305
+ let invocations = environment.invocations.events_since(start.invocation);
306
+ RunMetadata {
200
307
  outcome,
201
- unsupported: environment.unsupported.values_since(unsupported_start),
308
+ unsupported: environment.unsupported.values_since(start.unsupported),
202
309
  dropped_unsupported: environment
203
310
  .unsupported
204
311
  .dropped()
205
- .saturating_sub(dropped_unsupported_start),
206
- commands: environment.cmd_trace.values_since(command_start),
312
+ .saturating_sub(start.dropped_unsupported),
313
+ commands: environment.cmd_trace.values_since(start.command),
207
314
  dropped_commands: environment
208
315
  .cmd_trace
209
316
  .dropped()
210
- .saturating_sub(dropped_commands_start),
211
- noop_commands: invocation_names(&invocations, CommandTrust::NoOp),
317
+ .saturating_sub(start.dropped_commands),
318
+ unsupported_commands: invocation_names(&invocations, CommandTrust::Unsupported),
212
319
  partial_commands: invocation_names(&invocations, CommandTrust::Partial),
213
320
  invocations,
214
321
  dropped_invocations: environment
215
322
  .invocations
216
323
  .dropped()
217
- .saturating_sub(dropped_invocations_start),
218
- };
219
- (metadata, stdout, stderr)
324
+ .saturating_sub(start.dropped_invocations),
325
+ network_requests: environment.net.log[start.network..].to_vec(),
326
+ dropped_network_requests: environment
327
+ .net
328
+ .dropped_requests
329
+ .saturating_sub(start.dropped_network),
330
+ }
220
331
  }
221
332
 
222
333
  fn invocation_names(events: &[InvocationEvent], trust: CommandTrust) -> Vec<String> {
@@ -1,8 +1,11 @@
1
1
  """Typed Python interface to shellsim's deterministic execution environment."""
2
2
 
3
+ from . import python as python
3
4
  from ._api import (
4
5
  CommandUsage,
5
6
  Environment,
7
+ HttpRequest,
8
+ HttpResponse,
6
9
  Invocation,
7
10
  Limits,
8
11
  MountResult,
@@ -15,6 +18,8 @@ from ._api import (
15
18
  __all__ = [
16
19
  "CommandUsage",
17
20
  "Environment",
21
+ "HttpRequest",
22
+ "HttpResponse",
18
23
  "Invocation",
19
24
  "Limits",
20
25
  "MountResult",
@@ -22,4 +27,5 @@ __all__ = [
22
27
  "SimulationError",
23
28
  "Usage",
24
29
  "run",
30
+ "python",
25
31
  ]
@@ -2,5 +2,4 @@
2
2
 
3
3
  from ._cli import main
4
4
 
5
-
6
5
  raise SystemExit(main())