shellsim 0.1.0__tar.gz → 0.1.3__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 (127) hide show
  1. {shellsim-0.1.0 → shellsim-0.1.3}/Cargo.lock +1 -1
  2. {shellsim-0.1.0 → shellsim-0.1.3}/Cargo.toml +4 -3
  3. shellsim-0.1.3/PKG-INFO +119 -0
  4. shellsim-0.1.3/README.md +95 -0
  5. {shellsim-0.1.0 → shellsim-0.1.3}/pyproject.toml +27 -2
  6. {shellsim-0.1.0 → shellsim-0.1.3}/python/native/Cargo.lock +2 -2
  7. {shellsim-0.1.0 → shellsim-0.1.3}/python/native/Cargo.toml +5 -1
  8. shellsim-0.1.3/python/shellsim/__main__.py +5 -0
  9. {shellsim-0.1.0 → shellsim-0.1.3}/python/shellsim/_api.py +7 -17
  10. shellsim-0.1.3/python/shellsim/_cli.py +175 -0
  11. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/builtins.rs +305 -64
  12. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/fs.rs +392 -72
  13. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/hashing.rs +76 -9
  14. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/mod.rs +77 -0
  15. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/printf.rs +45 -9
  16. shellsim-0.1.3/src/commands/sort.rs +146 -0
  17. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/system.rs +119 -25
  18. shellsim-0.1.3/src/commands/text.rs +1976 -0
  19. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/util.rs +13 -1
  20. {shellsim-0.1.0 → shellsim-0.1.3}/src/descriptors.rs +121 -0
  21. {shellsim-0.1.0 → shellsim-0.1.3}/src/exec.rs +97 -25
  22. {shellsim-0.1.0 → shellsim-0.1.3}/src/expand.rs +64 -7
  23. {shellsim-0.1.0 → shellsim-0.1.3}/src/interp.rs +191 -0
  24. {shellsim-0.1.0 → shellsim-0.1.3}/src/pseudo_fs.rs +20 -2
  25. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/heap.rs +18 -1
  26. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/mod.rs +23 -0
  27. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/native.rs +138 -0
  28. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/number.rs +1 -1
  29. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/object_model.rs +154 -4
  30. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/parser.rs +38 -1
  31. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/protocol.rs +12 -2
  32. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/mod.rs +8 -1
  33. shellsim-0.1.3/src/python/stdlib/numpy.rs +3951 -0
  34. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/vm.rs +382 -18
  35. {shellsim-0.1.0 → shellsim-0.1.3}/src/resources.rs +5 -0
  36. shellsim-0.1.0/PKG-INFO +0 -317
  37. shellsim-0.1.0/README.md +0 -293
  38. shellsim-0.1.0/src/commands/sort.rs +0 -55
  39. shellsim-0.1.0/src/commands/text.rs +0 -1148
  40. {shellsim-0.1.0 → shellsim-0.1.3}/LICENSE +0 -0
  41. {shellsim-0.1.0 → shellsim-0.1.3}/python/native/src/lib.rs +0 -0
  42. {shellsim-0.1.0 → shellsim-0.1.3}/python/shellsim/__init__.py +0 -0
  43. {shellsim-0.1.0 → shellsim-0.1.3}/python/shellsim/py.typed +0 -0
  44. {shellsim-0.1.0 → shellsim-0.1.3}/rust-toolchain.toml +0 -0
  45. {shellsim-0.1.0 → shellsim-0.1.3}/src/bin/shellsim-python.rs +0 -0
  46. {shellsim-0.1.0 → shellsim-0.1.3}/src/clock.rs +0 -0
  47. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/archives.rs +0 -0
  48. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/awk.rs +0 -0
  49. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/echo.rs +0 -0
  50. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/git.rs +0 -0
  51. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/makecmd.rs +0 -0
  52. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/net.rs +0 -0
  53. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/patch.rs +0 -0
  54. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/pkg.rs +0 -0
  55. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/proc.rs +0 -0
  56. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/rgcmd.rs +0 -0
  57. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/tarcmd.rs +0 -0
  58. {shellsim-0.1.0 → shellsim-0.1.3}/src/commands/zipcmd.rs +0 -0
  59. {shellsim-0.1.0 → shellsim-0.1.3}/src/harness.rs +0 -0
  60. {shellsim-0.1.0 → shellsim-0.1.3}/src/harness_manager.rs +0 -0
  61. {shellsim-0.1.0 → shellsim-0.1.3}/src/hashes.rs +0 -0
  62. {shellsim-0.1.0 → shellsim-0.1.3}/src/host_ingest.rs +0 -0
  63. {shellsim-0.1.0 → shellsim-0.1.3}/src/jqcmd.rs +0 -0
  64. {shellsim-0.1.0 → shellsim-0.1.3}/src/lib.rs +0 -0
  65. {shellsim-0.1.0 → shellsim-0.1.3}/src/main.rs +0 -0
  66. {shellsim-0.1.0 → shellsim-0.1.3}/src/mcp.rs +0 -0
  67. {shellsim-0.1.0 → shellsim-0.1.3}/src/net.rs +0 -0
  68. {shellsim-0.1.0 → shellsim-0.1.3}/src/netcmd.rs +0 -0
  69. {shellsim-0.1.0 → shellsim-0.1.3}/src/process.rs +0 -0
  70. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/ast.rs +0 -0
  71. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/bytecode.rs +0 -0
  72. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/compiler.rs +0 -0
  73. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/filesystem.rs +0 -0
  74. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/lexer.rs +0 -0
  75. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/process.rs +0 -0
  76. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/source.rs +0 -0
  77. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/argparse.rs +0 -0
  78. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/base64.rs +0 -0
  79. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/bisect.rs +0 -0
  80. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/collections.rs +0 -0
  81. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/core.rs +0 -0
  82. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/dataclasses.rs +0 -0
  83. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/enum.rs +0 -0
  84. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/frozen.rs +0 -0
  85. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/functools.rs +0 -0
  86. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/hashlib.rs +0 -0
  87. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/heapq.rs +0 -0
  88. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/itertools.rs +0 -0
  89. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/json.rs +0 -0
  90. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/math.rs +0 -0
  91. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/os.rs +0 -0
  92. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/pytest.rs +0 -0
  93. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/re.rs +0 -0
  94. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/abc.py +0 -0
  95. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/base64.py +0 -0
  96. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/codecs.py +0 -0
  97. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/collections.py +0 -0
  98. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/csv.py +0 -0
  99. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/datetime.py +0 -0
  100. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/glob.py +0 -0
  101. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/hashlib.py +0 -0
  102. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/io.py +0 -0
  103. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/json.py +0 -0
  104. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/logging.py +0 -0
  105. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/os.py +0 -0
  106. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/pathlib.py +0 -0
  107. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/struct.py +0 -0
  108. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/subprocess.py +0 -0
  109. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/tempfile.py +0 -0
  110. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/uuid.py +0 -0
  111. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/source/zlib.py +0 -0
  112. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/string.rs +0 -0
  113. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/struct.rs +0 -0
  114. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/subprocess.rs +0 -0
  115. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/sys.rs +0 -0
  116. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/time.rs +0 -0
  117. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/typing.rs +0 -0
  118. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/unittest.rs +0 -0
  119. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/vfs.rs +0 -0
  120. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/stdlib/zlib.rs +0 -0
  121. {shellsim-0.1.0 → shellsim-0.1.3}/src/python/token.rs +0 -0
  122. {shellsim-0.1.0 → shellsim-0.1.3}/src/sandbox.rs +0 -0
  123. {shellsim-0.1.0 → shellsim-0.1.3}/src/scenario.rs +0 -0
  124. {shellsim-0.1.0 → shellsim-0.1.3}/src/scheduler.rs +0 -0
  125. {shellsim-0.1.0 → shellsim-0.1.3}/src/shell.rs +0 -0
  126. {shellsim-0.1.0 → shellsim-0.1.3}/src/telemetry.rs +0 -0
  127. {shellsim-0.1.0 → shellsim-0.1.3}/src/vfs.rs +0 -0
@@ -330,7 +330,7 @@ dependencies = [
330
330
 
331
331
  [[package]]
332
332
  name = "shellsim"
333
- version = "0.1.0"
333
+ version = "0.1.3"
334
334
  dependencies = [
335
335
  "base64",
336
336
  "crc32fast",
@@ -1,8 +1,8 @@
1
1
  [package]
2
2
  name = "shellsim"
3
- version = "0.1.0"
3
+ version = "0.1.3"
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,4 +58,5 @@ python = []
58
58
  opt-level = 1
59
59
 
60
60
  [profile.release]
61
- opt-level = 2
61
+ opt-level = "s"
62
+ lto = false
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: shellsim
3
+ Version: 0.1.3
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
+ package installation, compilers, and arbitrary machine code are outside the simulation boundary.
57
+ See [Python in shellsim](docs/python.md) for the current contract.
58
+
59
+ ## Install and run
60
+
61
+ Install the Python package and console command:
62
+
63
+ ```sh
64
+ python -m pip install shellsim
65
+ shellsim -c 'printf "b\na\n" | sort'
66
+ shellsim --root ./project -c 'python3.14 test.py'
67
+ ```
68
+
69
+ `--root` copies the selected host tree into a disposable `/work` snapshot. With no `-c` and a
70
+ terminal attached, `shellsim` starts a persistent interactive session.
71
+
72
+ To build the Rust binaries from source:
73
+
74
+ ```sh
75
+ cargo build --release
76
+ ./target/release/shellsim -c 'echo hello'
77
+ ./target/release/shellsim eval --cpu 100k --memory 8m -c 'make test'
78
+ ./target/release/shellsim-python ./project/main.py -- arg1
79
+ ```
80
+
81
+ Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
82
+ exit status, stdout and stderr, resource use, command trace, and unsupported behavior.
83
+
84
+ The Python API exposes fresh and persistent environments:
85
+
86
+ ```python
87
+ import shellsim
88
+
89
+ environment = shellsim.Environment(cpu=100_000)
90
+ environment.write_file("/work/main.py", "print(6 * 7)\n")
91
+ result = environment.run("python3.14 /work/main.py")
92
+ assert result.returncode == 0
93
+ assert result.stdout == b"42\n"
94
+ ```
95
+
96
+ ## Agent harness
97
+
98
+ `shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
99
+ bounded execution, streaming actions, VFS operations, checkpoints, workspace diffs, process and
100
+ resource inspection, and deterministic session forks. `shellsim mcp` exposes the same environment
101
+ as a stdio MCP server. `shellsim replay scenario.ndjson` reruns checked action transcripts.
102
+
103
+ ```sh
104
+ printf '%s\n' \
105
+ '{"id":1,"op":"execute","source":"printf hello > result"}' \
106
+ '{"id":2,"op":"workspace_diff"}' \
107
+ | shellsim serve --root ./project
108
+ ```
109
+
110
+ ## Resource model
111
+
112
+ CPU is deterministic fuel, memory is modeled working set, disk is current virtual-filesystem
113
+ usage, and output bounds materialized stdout and stderr. The defaults are 10,000,000 CPU units,
114
+ 64 MiB memory, 64 MiB disk, and 4 MiB output. Costs are stable and intentionally approximate.
115
+ Exhaustion is observable and never falls back to an ambient host implementation.
116
+
117
+ For internals and contribution workflow, see [implementation](docs/implementation.md),
118
+ [Python](docs/python.md), and [CONTRIBUTING.md](CONTRIBUTING.md).
119
+
@@ -0,0 +1,95 @@
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
+ package installation, compilers, and arbitrary machine code are outside the simulation boundary.
34
+ See [Python in shellsim](docs/python.md) for the current contract.
35
+
36
+ ## Install and run
37
+
38
+ Install the Python package and console command:
39
+
40
+ ```sh
41
+ python -m pip install shellsim
42
+ shellsim -c 'printf "b\na\n" | sort'
43
+ shellsim --root ./project -c 'python3.14 test.py'
44
+ ```
45
+
46
+ `--root` copies the selected host tree into a disposable `/work` snapshot. With no `-c` and a
47
+ terminal attached, `shellsim` starts a persistent interactive session.
48
+
49
+ To build the Rust binaries from source:
50
+
51
+ ```sh
52
+ cargo build --release
53
+ ./target/release/shellsim -c 'echo hello'
54
+ ./target/release/shellsim eval --cpu 100k --memory 8m -c 'make test'
55
+ ./target/release/shellsim-python ./project/main.py -- arg1
56
+ ```
57
+
58
+ Limits accept `k`, `m`, and `g` binary suffixes. `eval` emits a structured result containing the
59
+ exit status, stdout and stderr, resource use, command trace, and unsupported behavior.
60
+
61
+ The Python API exposes fresh and persistent environments:
62
+
63
+ ```python
64
+ import shellsim
65
+
66
+ environment = shellsim.Environment(cpu=100_000)
67
+ environment.write_file("/work/main.py", "print(6 * 7)\n")
68
+ result = environment.run("python3.14 /work/main.py")
69
+ assert result.returncode == 0
70
+ assert result.stdout == b"42\n"
71
+ ```
72
+
73
+ ## Agent harness
74
+
75
+ `shellsim serve --root ./project` runs a persistent newline-delimited JSON session. It supports
76
+ bounded execution, streaming actions, VFS operations, checkpoints, workspace diffs, process and
77
+ resource inspection, and deterministic session forks. `shellsim mcp` exposes the same environment
78
+ as a stdio MCP server. `shellsim replay scenario.ndjson` reruns checked action transcripts.
79
+
80
+ ```sh
81
+ printf '%s\n' \
82
+ '{"id":1,"op":"execute","source":"printf hello > result"}' \
83
+ '{"id":2,"op":"workspace_diff"}' \
84
+ | shellsim serve --root ./project
85
+ ```
86
+
87
+ ## Resource model
88
+
89
+ CPU is deterministic fuel, memory is modeled working set, disk is current virtual-filesystem
90
+ usage, and output bounds materialized stdout and stderr. The defaults are 10,000,000 CPU units,
91
+ 64 MiB memory, 64 MiB disk, and 4 MiB output. Costs are stable and intentionally approximate.
92
+ Exhaustion is observable and never falls back to an ambient host implementation.
93
+
94
+ For internals and contribution workflow, see [implementation](docs/implementation.md),
95
+ [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,13 +22,16 @@ 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"
29
29
  Issues = "https://github.com/rjpower/shellsim/issues"
30
30
  Repository = "https://github.com/rjpower/shellsim"
31
31
 
32
+ [project.scripts]
33
+ shellsim = "shellsim._cli:main"
34
+
32
35
  [dependency-groups]
33
36
  test = ["pytest>=8"]
34
37
 
@@ -42,3 +45,25 @@ strip = true
42
45
 
43
46
  [tool.pytest.ini_options]
44
47
  testpaths = ["python_tests"]
48
+
49
+ [tool.marin-style]
50
+ checks = ["ruff-check", "ruff-format"]
51
+ include = [
52
+ "infra/*.py",
53
+ "infra/**/*.py",
54
+ "python/**/*.py",
55
+ "python_tests/*.py",
56
+ "python_tests/**/*.py",
57
+ "tools/*.py",
58
+ "tools/**/*.py",
59
+ ]
60
+ main_branch = "main"
61
+ ruff_version = "0.14.3"
62
+
63
+ [tool.ruff]
64
+ line-length = 120
65
+ target-version = "py39"
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "I", "B", "C4"]
69
+ ignore = ["E501"]
@@ -406,7 +406,7 @@ dependencies = [
406
406
 
407
407
  [[package]]
408
408
  name = "shellsim"
409
- version = "0.1.0"
409
+ version = "0.1.3"
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.0"
430
+ version = "0.1.3"
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.0"
5
+ version = "0.1.3"
6
6
  edition = "2021"
7
7
  description = "Python bindings for shellsim"
8
8
  license = "Apache-2.0"
@@ -18,3 +18,7 @@ pyo3 = { version = "0.28", features = ["abi3-py39", "extension-module"] }
18
18
  serde = { version = "1", features = ["derive"] }
19
19
  serde_json = "1"
20
20
  shellsim = { path = "../.." }
21
+
22
+ [profile.release]
23
+ opt-level = "s"
24
+ lto = false
@@ -0,0 +1,5 @@
1
+ """Run the shellsim package as a module."""
2
+
3
+ from ._cli import main
4
+
5
+ raise SystemExit(main())
@@ -2,14 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from dataclasses import dataclass
6
5
  import json
7
6
  import os
8
- from typing import Any, Mapping, Optional, Tuple, Union
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from typing import Any, Optional, Tuple, Union
9
10
 
10
11
  from . import _native
11
12
 
12
-
13
13
  SimulationError = _native.SimulationError
14
14
 
15
15
  _MAX_U64 = (1 << 64) - 1
@@ -111,9 +111,7 @@ class RunResult:
111
111
  if self.returncode != 0:
112
112
  diagnostic = self.stderr_text.strip()
113
113
  suffix = f": {diagnostic}" if diagnostic else ""
114
- raise SimulationError(
115
- f"shellsim action exited with status {self.returncode}{suffix}"
116
- )
114
+ raise SimulationError(f"shellsim action exited with status {self.returncode}{suffix}")
117
115
 
118
116
 
119
117
  class Environment:
@@ -138,11 +136,7 @@ class Environment:
138
136
  if limits is not None and not isinstance(limits, Limits):
139
137
  raise TypeError("limits must be a shellsim.Limits instance")
140
138
  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
- }
139
+ **{name: _validate_limit(name, value) for name, value in overrides.items() if value is not None}
146
140
  )
147
141
  self._native = _native.NativeEnvironment(
148
142
  resolved.cpu,
@@ -157,9 +151,7 @@ class Environment:
157
151
 
158
152
  return bool(self._native.terminated)
159
153
 
160
- def run(
161
- self, source: str, stdin: Union[bytes, bytearray, memoryview] = b""
162
- ) -> RunResult:
154
+ def run(self, source: str, stdin: Union[bytes, bytearray, memoryview] = b"") -> RunResult:
163
155
  """Execute one complete shell action with an explicit input byte stream."""
164
156
 
165
157
  if not isinstance(source, str):
@@ -204,9 +196,7 @@ class Environment:
204
196
  raise TypeError("parents must be bool")
205
197
  self._native.mkdir(path, parents)
206
198
 
207
- def mount(
208
- self, host_root: Union[str, os.PathLike[str]], destination: str = "/work"
209
- ) -> MountResult:
199
+ def mount(self, host_root: Union[str, os.PathLike[str]], destination: str = "/work") -> MountResult:
210
200
  """Copy an explicitly trusted host directory into the bounded VFS.
211
201
 
212
202
  The walk rejects symlinks and non-regular files, has a 10,000-file limit, and skips
@@ -0,0 +1,175 @@
1
+ """Command-line interface for the installed shellsim Python package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from collections.abc import Sequence
8
+ from typing import Any, Optional
9
+
10
+ from ._api import Environment, RunResult, SimulationError
11
+
12
+ _MAX_U64 = (1 << 64) - 1
13
+
14
+
15
+ def _quantity(value: str) -> int:
16
+ """Parse a non-negative count with an optional binary k, m, or g suffix."""
17
+
18
+ suffixes = {"k": 1024, "m": 1024**2, "g": 1024**3}
19
+ suffix = value[-1:].lower()
20
+ multiplier = suffixes.get(suffix, 1)
21
+ digits = value[:-1] if suffix in suffixes else value
22
+ try:
23
+ number = int(digits)
24
+ except ValueError as error:
25
+ raise argparse.ArgumentTypeError("expected a count, optionally suffixed with k, m, or g") from error
26
+ result = number * multiplier
27
+ if number < 0 or result > _MAX_U64:
28
+ raise argparse.ArgumentTypeError(f"expected a count between 0 and {_MAX_U64}")
29
+ return result
30
+
31
+
32
+ def _parser() -> argparse.ArgumentParser:
33
+ """Build the command-line parser shared by the console and module entry points."""
34
+
35
+ parser = argparse.ArgumentParser(
36
+ prog="shellsim",
37
+ description="Run shell commands in a deterministic in-memory simulation.",
38
+ )
39
+ parser.add_argument("-c", "--command", metavar="SOURCE", help="execute one shell action")
40
+ parser.add_argument(
41
+ "--root",
42
+ metavar="DIR",
43
+ help="copy this trusted host directory into the simulated /work tree",
44
+ )
45
+ parser.add_argument("--cpu", type=_quantity, help="cumulative CPU fuel limit")
46
+ parser.add_argument("--memory", type=_quantity, help="modeled memory limit")
47
+ parser.add_argument("--disk", type=_quantity, help="simulated filesystem limit")
48
+ parser.add_argument("--output", type=_quantity, help="cumulative output limit")
49
+ return parser
50
+
51
+
52
+ def _binary_stream(stream: Any) -> Any:
53
+ """Return a stream that accepts bytes, including under in-process test doubles."""
54
+
55
+ return getattr(stream, "buffer", stream)
56
+
57
+
58
+ def _write_bytes(stream: Any, data: bytes) -> None:
59
+ """Write exact simulator bytes to a host console stream."""
60
+
61
+ binary = _binary_stream(stream)
62
+ try:
63
+ binary.write(data)
64
+ except TypeError:
65
+ binary.write(data.decode("utf-8", errors="replace"))
66
+ binary.flush()
67
+
68
+
69
+ def _emit_result(result: RunResult, stdout: Any, stderr: Any) -> None:
70
+ """Forward one simulated action's byte-preserving output."""
71
+
72
+ _write_bytes(stdout, result.stdout)
73
+ _write_bytes(stderr, result.stderr)
74
+
75
+
76
+ def _execute_action(
77
+ environment: Environment,
78
+ source: str,
79
+ stdin: bytes,
80
+ stdout: Any,
81
+ stderr: Any,
82
+ ) -> Optional[RunResult]:
83
+ """Execute and forward one action, reporting adapter failures without a traceback."""
84
+
85
+ try:
86
+ result = environment.run(source, stdin)
87
+ except SimulationError as error:
88
+ print(f"shellsim: {error}", file=stderr)
89
+ return None
90
+ _emit_result(result, stdout, stderr)
91
+ return result
92
+
93
+
94
+ def _read_bytes(stream: Any) -> bytes:
95
+ """Read all remaining input without depending on a text-stream encoding."""
96
+
97
+ data = _binary_stream(stream).read()
98
+ return data.encode() if isinstance(data, str) else data
99
+
100
+
101
+ def _prepare_environment(options: argparse.Namespace, stdout: Any, stderr: Any) -> tuple[Optional[Environment], int]:
102
+ """Create a simulated machine, import an optional host snapshot, and enter `/work`."""
103
+
104
+ environment = Environment(
105
+ cpu=options.cpu,
106
+ memory=options.memory,
107
+ disk=options.disk,
108
+ output=options.output,
109
+ )
110
+ try:
111
+ if options.root is not None:
112
+ environment.mount(options.root)
113
+ prepared = environment.run("cd /work")
114
+ except SimulationError as error:
115
+ print(f"shellsim: {error}", file=stderr)
116
+ return None, 2
117
+
118
+ if prepared.returncode != 0:
119
+ _emit_result(prepared, stdout, stderr)
120
+ return None, prepared.returncode
121
+ return environment, 0
122
+
123
+
124
+ def _interactive(environment: Environment, stdin: Any, stdout: Any, stderr: Any) -> int:
125
+ """Run one action per input line while retaining simulated state."""
126
+
127
+ status = 0
128
+ while not environment.terminated:
129
+ stdout.write("shellsim$ ")
130
+ stdout.flush()
131
+ try:
132
+ line = stdin.readline()
133
+ except KeyboardInterrupt:
134
+ stdout.write("\n")
135
+ stdout.flush()
136
+ continue
137
+ if not line:
138
+ stdout.write("\n")
139
+ stdout.flush()
140
+ break
141
+ result = _execute_action(environment, line, b"", stdout, stderr)
142
+ if result is None:
143
+ return 1
144
+ status = result.returncode
145
+ return status
146
+
147
+
148
+ def main(arguments: Optional[Sequence[str]] = None) -> int:
149
+ """Run the package CLI and return the simulated or usage exit status."""
150
+
151
+ options = _parser().parse_args(sys.argv[1:] if arguments is None else arguments)
152
+ environment, preparation_status = _prepare_environment(options, sys.stdout, sys.stderr)
153
+ if environment is None:
154
+ return preparation_status
155
+
156
+ if options.command is not None:
157
+ stdin = b"" if sys.stdin.isatty() else _read_bytes(sys.stdin)
158
+ result = _execute_action(environment, options.command, stdin, sys.stdout, sys.stderr)
159
+ if result is None:
160
+ return 1
161
+ return result.returncode
162
+
163
+ if sys.stdin.isatty():
164
+ return _interactive(environment, sys.stdin, sys.stdout, sys.stderr)
165
+
166
+ source_bytes = _read_bytes(sys.stdin)
167
+ try:
168
+ source = source_bytes.decode("utf-8")
169
+ except UnicodeDecodeError as error:
170
+ print(f"shellsim: stdin is not valid UTF-8 shell source: {error}", file=sys.stderr)
171
+ return 2
172
+ result = _execute_action(environment, source, b"", sys.stdout, sys.stderr)
173
+ if result is None:
174
+ return 1
175
+ return result.returncode