shellsim 0.1.3__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 (177) hide show
  1. {shellsim-0.1.3 → shellsim-0.1.4}/Cargo.lock +1 -1
  2. {shellsim-0.1.3 → shellsim-0.1.4}/Cargo.toml +4 -3
  3. {shellsim-0.1.3 → shellsim-0.1.4}/PKG-INFO +52 -3
  4. {shellsim-0.1.3 → shellsim-0.1.4}/README.md +51 -2
  5. {shellsim-0.1.3 → shellsim-0.1.4}/pyproject.toml +4 -3
  6. {shellsim-0.1.3 → shellsim-0.1.4}/python/native/Cargo.lock +2 -2
  7. {shellsim-0.1.3 → shellsim-0.1.4}/python/native/Cargo.toml +4 -3
  8. {shellsim-0.1.3 → shellsim-0.1.4}/python/native/src/lib.rs +128 -17
  9. {shellsim-0.1.3 → shellsim-0.1.4}/python/shellsim/__init__.py +6 -0
  10. {shellsim-0.1.3 → shellsim-0.1.4}/python/shellsim/_api.py +121 -3
  11. shellsim-0.1.4/python/shellsim/python.py +32 -0
  12. shellsim-0.1.4/src/commands/awk/ast.rs +146 -0
  13. shellsim-0.1.4/src/commands/awk/eval.rs +1180 -0
  14. shellsim-0.1.4/src/commands/awk/lexer.rs +367 -0
  15. shellsim-0.1.4/src/commands/awk/parser.rs +522 -0
  16. shellsim-0.1.4/src/commands/awk.rs +243 -0
  17. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/builtins.rs +155 -55
  18. shellsim-0.1.4/src/commands/dd.rs +308 -0
  19. shellsim-0.1.4/src/commands/find.rs +323 -0
  20. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/fs.rs +47 -172
  21. shellsim-0.1.4/src/commands/git/apply.rs +255 -0
  22. shellsim-0.1.4/src/commands/git/commit.rs +339 -0
  23. shellsim-0.1.4/src/commands/git/compare.rs +731 -0
  24. shellsim-0.1.4/src/commands/git/config.rs +356 -0
  25. shellsim-0.1.4/src/commands/git/conflict.rs +564 -0
  26. shellsim-0.1.4/src/commands/git/diff.rs +609 -0
  27. shellsim-0.1.4/src/commands/git/ignore.rs +381 -0
  28. shellsim-0.1.4/src/commands/git/log.rs +1055 -0
  29. shellsim-0.1.4/src/commands/git/merge.rs +722 -0
  30. shellsim-0.1.4/src/commands/git/mod.rs +794 -0
  31. shellsim-0.1.4/src/commands/git/plumbing.rs +1215 -0
  32. shellsim-0.1.4/src/commands/git/rebase.rs +475 -0
  33. shellsim-0.1.4/src/commands/git/refs.rs +699 -0
  34. shellsim-0.1.4/src/commands/git/repo.rs +1210 -0
  35. shellsim-0.1.4/src/commands/git/stash.rs +441 -0
  36. shellsim-0.1.4/src/commands/git/switch.rs +522 -0
  37. shellsim-0.1.4/src/commands/git/worktree.rs +1592 -0
  38. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/mod.rs +56 -6
  39. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/net.rs +14 -4
  40. shellsim-0.1.4/src/commands/options.rs +271 -0
  41. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/patch.rs +100 -23
  42. shellsim-0.1.4/src/commands/pkg.rs +304 -0
  43. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/proc.rs +220 -92
  44. shellsim-0.1.4/src/commands/regex_compat.rs +56 -0
  45. shellsim-0.1.4/src/commands/sed.rs +787 -0
  46. shellsim-0.1.4/src/commands/sort.rs +255 -0
  47. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/system.rs +1 -1
  48. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/text.rs +389 -385
  49. shellsim-0.1.4/src/commands/unavailable.rs +39 -0
  50. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/util.rs +24 -1
  51. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/zipcmd.rs +12 -1
  52. {shellsim-0.1.3 → shellsim-0.1.4}/src/exec.rs +108 -7
  53. {shellsim-0.1.3 → shellsim-0.1.4}/src/harness.rs +3 -3
  54. {shellsim-0.1.3 → shellsim-0.1.4}/src/interp.rs +37 -9
  55. shellsim-0.1.4/src/jqcmd.rs +1497 -0
  56. {shellsim-0.1.3 → shellsim-0.1.4}/src/main.rs +3 -3
  57. shellsim-0.1.4/src/net.rs +550 -0
  58. shellsim-0.1.4/src/netcmd.rs +357 -0
  59. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/ast.rs +16 -3
  60. shellsim-0.1.4/src/python/bytecode.rs +511 -0
  61. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/compiler.rs +284 -75
  62. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/filesystem.rs +81 -32
  63. shellsim-0.1.4/src/python/heap.rs +1795 -0
  64. shellsim-0.1.4/src/python/http.rs +135 -0
  65. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/lexer.rs +48 -3
  66. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/mod.rs +279 -46
  67. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/native.rs +131 -48
  68. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/number.rs +223 -79
  69. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/object_model.rs +156 -77
  70. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/parser.rs +310 -92
  71. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/protocol.rs +167 -12
  72. shellsim-0.1.4/src/python/slice.rs +133 -0
  73. shellsim-0.1.4/src/python/stdlib/argparse.rs +674 -0
  74. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/core.rs +596 -25
  75. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/frozen.rs +10 -0
  76. shellsim-0.1.4/src/python/stdlib/http.rs +63 -0
  77. shellsim-0.1.4/src/python/stdlib/importlib.rs +43 -0
  78. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/json.rs +36 -5
  79. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/math.rs +36 -0
  80. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/mod.rs +4 -0
  81. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/numpy.rs +856 -77
  82. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/os.rs +15 -6
  83. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/csv.py +4 -0
  84. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/datetime.py +15 -0
  85. shellsim-0.1.4/src/python/stdlib/source/http.py +3 -0
  86. shellsim-0.1.4/src/python/stdlib/source/http_client.py +271 -0
  87. shellsim-0.1.4/src/python/stdlib/source/importlib.py +47 -0
  88. shellsim-0.1.4/src/python/stdlib/source/json.py +25 -0
  89. shellsim-0.1.4/src/python/stdlib/source/numpy_random.py +99 -0
  90. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/os.py +81 -8
  91. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/pathlib.py +36 -0
  92. shellsim-0.1.4/src/python/stdlib/source/random.py +97 -0
  93. shellsim-0.1.4/src/python/stdlib/source/statistics.py +127 -0
  94. shellsim-0.1.4/src/python/stdlib/source/urllib.py +4 -0
  95. shellsim-0.1.4/src/python/stdlib/source/urllib_error.py +50 -0
  96. shellsim-0.1.4/src/python/stdlib/source/urllib_request.py +155 -0
  97. shellsim-0.1.4/src/python/stdlib/source/zipfile.py +234 -0
  98. shellsim-0.1.4/src/python/stdlib/sys.rs +183 -0
  99. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/vfs.rs +46 -0
  100. shellsim-0.1.4/src/python/string.rs +170 -0
  101. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/token.rs +5 -0
  102. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/vm.rs +2389 -794
  103. {shellsim-0.1.3 → shellsim-0.1.4}/src/resources.rs +5 -0
  104. {shellsim-0.1.3 → shellsim-0.1.4}/src/scenario.rs +5 -5
  105. {shellsim-0.1.3 → shellsim-0.1.4}/src/shell.rs +142 -12
  106. {shellsim-0.1.3 → shellsim-0.1.4}/src/telemetry.rs +2 -2
  107. shellsim-0.1.3/src/commands/awk.rs +0 -674
  108. shellsim-0.1.3/src/commands/git.rs +0 -1858
  109. shellsim-0.1.3/src/commands/pkg.rs +0 -223
  110. shellsim-0.1.3/src/commands/sort.rs +0 -146
  111. shellsim-0.1.3/src/jqcmd.rs +0 -352
  112. shellsim-0.1.3/src/net.rs +0 -218
  113. shellsim-0.1.3/src/netcmd.rs +0 -135
  114. shellsim-0.1.3/src/python/bytecode.rs +0 -107
  115. shellsim-0.1.3/src/python/heap.rs +0 -586
  116. shellsim-0.1.3/src/python/stdlib/argparse.rs +0 -265
  117. shellsim-0.1.3/src/python/stdlib/source/json.py +0 -15
  118. shellsim-0.1.3/src/python/stdlib/sys.rs +0 -89
  119. {shellsim-0.1.3 → shellsim-0.1.4}/LICENSE +0 -0
  120. {shellsim-0.1.3 → shellsim-0.1.4}/python/shellsim/__main__.py +0 -0
  121. {shellsim-0.1.3 → shellsim-0.1.4}/python/shellsim/_cli.py +0 -0
  122. {shellsim-0.1.3 → shellsim-0.1.4}/python/shellsim/py.typed +0 -0
  123. {shellsim-0.1.3 → shellsim-0.1.4}/rust-toolchain.toml +0 -0
  124. {shellsim-0.1.3 → shellsim-0.1.4}/src/bin/shellsim-python.rs +0 -0
  125. {shellsim-0.1.3 → shellsim-0.1.4}/src/clock.rs +0 -0
  126. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/archives.rs +0 -0
  127. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/echo.rs +0 -0
  128. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/hashing.rs +0 -0
  129. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/makecmd.rs +0 -0
  130. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/printf.rs +0 -0
  131. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/rgcmd.rs +0 -0
  132. {shellsim-0.1.3 → shellsim-0.1.4}/src/commands/tarcmd.rs +0 -0
  133. {shellsim-0.1.3 → shellsim-0.1.4}/src/descriptors.rs +0 -0
  134. {shellsim-0.1.3 → shellsim-0.1.4}/src/expand.rs +0 -0
  135. {shellsim-0.1.3 → shellsim-0.1.4}/src/harness_manager.rs +0 -0
  136. {shellsim-0.1.3 → shellsim-0.1.4}/src/hashes.rs +0 -0
  137. {shellsim-0.1.3 → shellsim-0.1.4}/src/host_ingest.rs +0 -0
  138. {shellsim-0.1.3 → shellsim-0.1.4}/src/lib.rs +0 -0
  139. {shellsim-0.1.3 → shellsim-0.1.4}/src/mcp.rs +0 -0
  140. {shellsim-0.1.3 → shellsim-0.1.4}/src/process.rs +0 -0
  141. {shellsim-0.1.3 → shellsim-0.1.4}/src/pseudo_fs.rs +0 -0
  142. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/process.rs +0 -0
  143. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/source.rs +0 -0
  144. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/base64.rs +0 -0
  145. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/bisect.rs +0 -0
  146. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/collections.rs +0 -0
  147. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/dataclasses.rs +0 -0
  148. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/enum.rs +0 -0
  149. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/functools.rs +0 -0
  150. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/hashlib.rs +0 -0
  151. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/heapq.rs +0 -0
  152. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/itertools.rs +0 -0
  153. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/pytest.rs +0 -0
  154. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/re.rs +0 -0
  155. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/abc.py +0 -0
  156. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/base64.py +0 -0
  157. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/codecs.py +0 -0
  158. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/collections.py +0 -0
  159. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/glob.py +0 -0
  160. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/hashlib.py +0 -0
  161. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/io.py +0 -0
  162. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/logging.py +0 -0
  163. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/struct.py +0 -0
  164. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/subprocess.py +0 -0
  165. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/tempfile.py +0 -0
  166. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/uuid.py +0 -0
  167. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/source/zlib.py +0 -0
  168. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/string.rs +0 -0
  169. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/struct.rs +0 -0
  170. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/subprocess.rs +0 -0
  171. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/time.rs +0 -0
  172. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/typing.rs +0 -0
  173. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/unittest.rs +0 -0
  174. {shellsim-0.1.3 → shellsim-0.1.4}/src/python/stdlib/zlib.rs +0 -0
  175. {shellsim-0.1.3 → shellsim-0.1.4}/src/sandbox.rs +0 -0
  176. {shellsim-0.1.3 → shellsim-0.1.4}/src/scheduler.rs +0 -0
  177. {shellsim-0.1.3 → 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.3"
333
+ version = "0.1.4"
334
334
  dependencies = [
335
335
  "base64",
336
336
  "crc32fast",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "shellsim"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  edition = "2021"
5
5
  description = "In-process BusyBox-like environment for RL rollouts and agent tests"
6
6
  license = "Apache-2.0"
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shellsim
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: License :: OSI Approved :: Apache Software License
6
6
  Classifier: Operating System :: MacOS :: MacOS X
@@ -53,8 +53,25 @@ The current environment includes:
53
53
  third-party module surface.
54
54
 
55
55
  Python is source-compatible where supported, not ABI-compatible with CPython. Native extensions,
56
- package installation, compilers, and arbitrary machine code are outside the simulation boundary.
57
- See [Python in shellsim](docs/python.md) for the current contract.
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.
58
75
 
59
76
  ## Install and run
60
77
 
@@ -80,6 +97,8 @@ cargo build --release
80
97
 
81
98
  Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
82
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.
83
102
 
84
103
  The Python API exposes fresh and persistent environments:
85
104
 
@@ -93,6 +112,36 @@ assert result.returncode == 0
93
112
  assert result.stdout == b"42\n"
94
113
  ```
95
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
+
96
145
  ## Agent harness
97
146
 
98
147
  `shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
@@ -30,8 +30,25 @@ The current environment includes:
30
30
  third-party module surface.
31
31
 
32
32
  Python is source-compatible where supported, not ABI-compatible with CPython. Native extensions,
33
- package installation, compilers, and arbitrary machine code are outside the simulation boundary.
34
- See [Python in shellsim](docs/python.md) for the current contract.
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.
35
52
 
36
53
  ## Install and run
37
54
 
@@ -57,6 +74,8 @@ cargo build --release
57
74
 
58
75
  Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
59
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.
60
79
 
61
80
  The Python API exposes fresh and persistent environments:
62
81
 
@@ -70,6 +89,36 @@ assert result.returncode == 0
70
89
  assert result.stdout == b"42\n"
71
90
  ```
72
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
+
73
122
  ## Agent harness
74
123
 
75
124
  `shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
@@ -44,7 +44,7 @@ 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
48
 
49
49
  [tool.marin-style]
50
50
  checks = ["ruff-check", "ruff-format"]
@@ -52,8 +52,9 @@ include = [
52
52
  "infra/*.py",
53
53
  "infra/**/*.py",
54
54
  "python/**/*.py",
55
- "python_tests/*.py",
56
- "python_tests/**/*.py",
55
+ "tests/python/test_*.py",
56
+ "tests/python_package/**/*.py",
57
+ "tests/tooling/**/*.py",
57
58
  "tools/*.py",
58
59
  "tools/**/*.py",
59
60
  ]
@@ -406,7 +406,7 @@ dependencies = [
406
406
 
407
407
  [[package]]
408
408
  name = "shellsim"
409
- version = "0.1.3"
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.3"
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.3"
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
  ]
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import os
7
- from collections.abc import Mapping
7
+ from collections.abc import Mapping, Sequence
8
8
  from dataclasses import dataclass
9
9
  from typing import Any, Optional, Tuple, Union
10
10
 
@@ -64,6 +64,19 @@ class Invocation:
64
64
  unsupported_reason: Optional[str] = None
65
65
 
66
66
 
67
+ @dataclass(frozen=True)
68
+ class HttpRequest:
69
+ """One bounded request record emitted by the simulated HTTP broker."""
70
+
71
+ method: str
72
+ url: str
73
+ headers: Tuple[Tuple[str, str], ...]
74
+ dropped_headers: int
75
+ body_bytes: int
76
+ matched: bool
77
+ response_status: Optional[int]
78
+
79
+
67
80
  @dataclass(frozen=True)
68
81
  class MountResult:
69
82
  """Result of copying an explicitly trusted host tree into the VFS."""
@@ -72,6 +85,15 @@ class MountResult:
72
85
  skipped_directories: Tuple[str, ...]
73
86
 
74
87
 
88
+ @dataclass(frozen=True)
89
+ class HttpResponse:
90
+ """One static response returned by the simulated HTTP broker."""
91
+
92
+ status: int = 200
93
+ headers: Union[Mapping[str, str], Sequence[Tuple[str, str]]] = ()
94
+ body: Union[bytes, bytearray, memoryview, str] = b""
95
+
96
+
75
97
  @dataclass(frozen=True)
76
98
  class RunResult:
77
99
  """Byte-preserving output and structured fidelity/resource telemetry for one action."""
@@ -88,10 +110,12 @@ class RunResult:
88
110
  dropped_unsupported: int
89
111
  commands: Tuple[str, ...]
90
112
  dropped_commands: int
91
- noop_commands: Tuple[str, ...]
113
+ unsupported_commands: Tuple[str, ...]
92
114
  partial_commands: Tuple[str, ...]
93
115
  invocations: Tuple[Invocation, ...]
94
116
  dropped_invocations: int
117
+ network_requests: Tuple[HttpRequest, ...]
118
+ dropped_network_requests: int
95
119
 
96
120
  @property
97
121
  def stdout_text(self) -> str:
@@ -129,6 +153,7 @@ class Environment:
129
153
  memory: Optional[int] = None,
130
154
  disk: Optional[int] = None,
131
155
  output: Optional[int] = None,
156
+ http: Optional[Mapping[str, HttpResponse]] = None,
132
157
  ) -> None:
133
158
  overrides = {"cpu": cpu, "memory": memory, "disk": disk, "output": output}
134
159
  if limits is not None and any(value is not None for value in overrides.values()):
@@ -144,6 +169,11 @@ class Environment:
144
169
  resolved.disk,
145
170
  resolved.output,
146
171
  )
172
+ if http is not None:
173
+ if not isinstance(http, Mapping):
174
+ raise TypeError("http must be a mapping from URL patterns to shellsim.HttpResponse")
175
+ for pattern, response in http.items():
176
+ self.route_http(pattern, response)
147
177
 
148
178
  @property
149
179
  def terminated(self) -> bool:
@@ -159,6 +189,32 @@ class Environment:
159
189
  metadata, stdout, stderr = self._native.run(source, _as_bytes("stdin", stdin))
160
190
  return _decode_result(metadata, stdout, stderr)
161
191
 
192
+ def run_python(
193
+ self,
194
+ source: str,
195
+ argv: Sequence[str] = (),
196
+ stdin: Union[bytes, bytearray, memoryview] = b"",
197
+ ) -> RunResult:
198
+ """Execute Python source directly in this environment.
199
+
200
+ The Python interpreter is fresh for each call. The simulated filesystem, cwd, resource
201
+ usage, and terminal exhaustion state belong to the persistent environment.
202
+ """
203
+
204
+ if not isinstance(source, str):
205
+ raise TypeError("source must be str")
206
+ if isinstance(argv, (str, bytes, bytearray, memoryview)) or not isinstance(argv, Sequence):
207
+ raise TypeError("argv must be a sequence of str")
208
+ arguments = list(argv)
209
+ if not all(isinstance(argument, str) for argument in arguments):
210
+ raise TypeError("argv must be a sequence of str")
211
+ metadata, stdout, stderr = self._native.run_python(
212
+ source,
213
+ arguments,
214
+ _as_bytes("stdin", stdin),
215
+ )
216
+ return _decode_result(metadata, stdout, stderr)
217
+
162
218
  def write_file(
163
219
  self,
164
220
  path: str,
@@ -196,6 +252,34 @@ class Environment:
196
252
  raise TypeError("parents must be bool")
197
253
  self._native.mkdir(path, parents)
198
254
 
255
+ def route_http(
256
+ self,
257
+ pattern: str,
258
+ response: HttpResponse,
259
+ *,
260
+ method: Optional[str] = None,
261
+ ) -> None:
262
+ """Register a static HTTP response for an exact URL or ``*`` glob.
263
+
264
+ Requests are handled inside the simulator. This does not grant the environment DNS,
265
+ sockets, TLS, or access to the host network. When ``method`` is omitted, the route matches
266
+ every HTTP method.
267
+ """
268
+
269
+ if not isinstance(pattern, str):
270
+ raise TypeError("pattern must be str")
271
+ if not isinstance(response, HttpResponse):
272
+ raise TypeError("response must be a shellsim.HttpResponse instance")
273
+ if method is not None and not isinstance(method, str):
274
+ raise TypeError("method must be str or None")
275
+ if isinstance(response.status, bool) or not isinstance(response.status, int):
276
+ raise TypeError("response status must be int")
277
+ if not 0 <= response.status <= 0xFFFF:
278
+ raise ValueError("response status must fit in an unsigned 16-bit integer")
279
+ headers = _http_headers(response.headers)
280
+ body = response.body.encode() if isinstance(response.body, str) else _as_bytes("response body", response.body)
281
+ self._native.route_http(pattern, method, response.status, headers, body)
282
+
199
283
  def mount(self, host_root: Union[str, os.PathLike[str]], destination: str = "/work") -> MountResult:
200
284
  """Copy an explicitly trusted host directory into the bounded VFS.
201
285
 
@@ -225,6 +309,7 @@ def run(
225
309
  memory: Optional[int] = None,
226
310
  disk: Optional[int] = None,
227
311
  output: Optional[int] = None,
312
+ http: Optional[Mapping[str, HttpResponse]] = None,
228
313
  ) -> RunResult:
229
314
  """Execute one action in a fresh environment."""
230
315
 
@@ -234,6 +319,7 @@ def run(
234
319
  memory=memory,
235
320
  disk=disk,
236
321
  output=output,
322
+ http=http,
237
323
  ).run(source, stdin)
238
324
 
239
325
 
@@ -251,6 +337,24 @@ def _as_bytes(name: str, value: Any) -> bytes:
251
337
  return bytes(value)
252
338
 
253
339
 
340
+ def _http_headers(value: Any) -> list[Tuple[str, str]]:
341
+ if isinstance(value, Mapping):
342
+ headers = list(value.items())
343
+ elif isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray, memoryview)):
344
+ headers = list(value)
345
+ else:
346
+ raise TypeError("response headers must be a mapping or sequence of name/value pairs")
347
+ normalized = []
348
+ for header in headers:
349
+ if not isinstance(header, Sequence) or isinstance(header, (str, bytes)) or len(header) != 2:
350
+ raise TypeError("response headers must contain name/value pairs")
351
+ name, item = header
352
+ if not isinstance(name, str) or not isinstance(item, str):
353
+ raise TypeError("response header names and values must be str")
354
+ normalized.append((name, item))
355
+ return normalized
356
+
357
+
254
358
  def _decode_result(metadata_json: str, stdout: bytes, stderr: bytes) -> RunResult:
255
359
  metadata: Mapping[str, Any] = json.loads(metadata_json)
256
360
  outcome = metadata["outcome"]
@@ -270,6 +374,18 @@ def _decode_result(metadata_json: str, stdout: bytes, stderr: bytes) -> RunResul
270
374
  )
271
375
  for item in metadata["invocations"]
272
376
  )
377
+ network_requests = tuple(
378
+ HttpRequest(
379
+ method=item["method"],
380
+ url=item["url"],
381
+ headers=tuple(tuple(header) for header in item["headers"]),
382
+ dropped_headers=item["dropped_headers"],
383
+ body_bytes=item["body_bytes"],
384
+ matched=item["matched"],
385
+ response_status=item["response_status"],
386
+ )
387
+ for item in metadata["network_requests"]
388
+ )
273
389
  return RunResult(
274
390
  returncode=outcome["exit_status"],
275
391
  stdout=stdout,
@@ -283,8 +399,10 @@ def _decode_result(metadata_json: str, stdout: bytes, stderr: bytes) -> RunResul
283
399
  dropped_unsupported=metadata["dropped_unsupported"],
284
400
  commands=tuple(metadata["commands"]),
285
401
  dropped_commands=metadata["dropped_commands"],
286
- noop_commands=tuple(metadata["noop_commands"]),
402
+ unsupported_commands=tuple(metadata["unsupported_commands"]),
287
403
  partial_commands=tuple(metadata["partial_commands"]),
288
404
  invocations=invocations,
289
405
  dropped_invocations=metadata["dropped_invocations"],
406
+ network_requests=network_requests,
407
+ dropped_network_requests=metadata["dropped_network_requests"],
290
408
  )